diff --git a/README.md b/README.md index 3a1ae771..da0c7031 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ AgileTs -> Global, simple, spacy State and Logic Framework +> Global State and Logic Framework
@@ -42,20 +42,20 @@ ```tsx // -- core.js ------------------------------------------ -// 1️⃣ Create Instance of AgileTs -const App = new Agile(); - -// 2️⃣ Create State with help of before defined Agile Instance -const MY_FIRST_STATE = App.createState("Hello Friend!"); +// 1️⃣ Create State with the initial value "Hello Friend!" +const MY_FIRST_STATE = createState("Hello Friend!"); // -- MyComponent.whatever ------------------------------------------ -// 3️⃣ Bind initialized State to desired UI-Component -// And wolla, it's reactive. Everytime the State mutates the Component rerenders -const myFirstState = useAgile(MY_FIRST_STATE); // Returns value of State ("Hello Friend!") +// 2️⃣ Bind initialized State to the desired UI-Component. +// And wolla, the Component is reactive. +// Everytime the State mutates the Component re-renders. +const myFirstState = useAgile(MY_FIRST_STATE); +console.log(myFirstState); // Returns "Hello Friend!" ``` -Want to learn more? Check out our [Quick Start Guides](https://agile-ts.org/docs/Installation.md). +Want to learn how to implement AgileTs in your preferred UI-Framework? +Check out our [Quick Start Guides](https://agile-ts.org/docs/Installation.md). ### ⛳️ Sandbox Test AgileTs yourself in a [codesandbox](https://codesandbox.io/s/agilets-first-state-f12cz). @@ -75,59 +75,56 @@ More examples can be found in the [Example Section](https://agile-ts.org/docs/ex
Why should I use AgileTs? -AgileTs is a global, simple, well-tested State Management Framework implemented in Typescript. +AgileTs is a global State and Logic Framework implemented in Typescript. It offers a reimagined API that focuses on **developer experience** -and allows you to **easily** manage your States. -Besides States, AgileTs offers some other powerful APIs that make your life easier. +and allows you to **easily** and **flexible** manage your application States. +Besides [States](https://agile-ts.org/docs/core/state), +AgileTs offers some other powerful APIs that make your life easier, +such as [Collections](https://agile-ts.org/docs/core/collection) +and [Computed States](https://agile-ts.org/docs/core/computed). The philosophy behind AgileTs is simple: ### 🚅 Straightforward Write minimalistic, boilerplate-free code that captures your intent. ```ts -const MY_STATE = App.createState('frank'); // Create State -MY_STATE.set('jeff'); // Update State value -MY_STATE.undo(); // Undo latest State value change -MY_STATE.is({hello: "jeff"}); // Check if State has the value '{hello: "jeff"}' -MY_STATE.watch((value) => {console.log(value);}); // Watch on State changes -``` +// Create State with inital value 'frank' +const MY_STATE = createState('frank'); -**Some more straightforward syntax examples:** +// Update State value from 'frank' to 'jeff' +MY_STATE.set('jeff'); -- Store State in any Storage, like the [Local Storage](https://www.w3schools.com/html/html5_webstorage.asp) - ```ts - MY_STATE.persist("storage-key"); - ``` -- Create a reactive Array of States - ```ts - const MY_COLLECTION = App.createCollection(); - MY_COLLECTION.collect({id: 1, name: "Frank"}); - MY_COLLECTION.collect({id: 2, name: "Dieter"}); - MY_COLLECTION.update(1, {name: "Jeff"}); - ``` -- Compute State depending on other States - ```ts - const MY_INTRODUCTION = App.createComputed(() => { - return `Hello I am '${MY_NAME.vale}' and I use ${MY_STATE_MANAGER.value} for State Management.`; - }); - ``` +// Undo latest State value change +MY_STATE.undo(); + +// Reset State value to its initial value +MY_STATE.reset(); + +// Permanently store State value in an external Storage +MY_STATE.persist("storage-key"); +``` ### 🤸‍ Flexible -- Works in nearly any UI-Layer. Check [here](https://agile-ts.org/docs/Frameworks) if your preferred Framework is supported too. -- Surly behaves with the workflow which suits you best. No need for _reducers_, _actions_, .. -- Has **0** external dependencies +- Works in nearly any UI-Framework (currently supported are React, React-Native and Vue). +- Surly behaves with the workflow that suits you best. + No need for _reducers_, _actions_, .. +- Has **0** external dependencies. ### ⛳️ Centralize -AgileTs is designed to take all business logic out of UI-Components and put them in a central place, often called `core`. -The benefit of keeping logic separate to UI-Components is to make your code more decoupled, portable, scalable, and above all, easily testable. +AgileTs is designed to take all business logic out of the UI-Components +and put them in a central place, often called `core`. +The benefit of keeping logic separate to UI-Components, +is to make your code more decoupled, portable, scalable, +and above all, easily testable. ### 🎯 Easy to Use -Learn the powerful tools of AgileTs in a short amount of time. An excellent place to start are -our [Quick Start Guides](https://agile-ts.org/docs/Installation), or if you don't like to follow any tutorials, -you can jump straight into our [Example](https://agile-ts.org/docs/examples/Introduction) Section. +Learn the powerful tools of AgileTs in a short amount of time. +An excellent place to start are our [Quick Start Guides](https://agile-ts.org/docs/Installation), +or if you don't like to follow any tutorials, +you can jump straight into our [Example Section](https://agile-ts.org/docs/examples/Introduction).
@@ -136,17 +133,18 @@ you can jump straight into our [Example](https://agile-ts.org/docs/examples/Intr
Installation -In order to properly use AgileTs, in a UI-Framework, we need to install **two** packages. +In order to use AgileTs in a UI-Framework, we need to install two packages. -- The [`core`](https://agile-ts.org/docs/core) package, which contains the State Management Logic of AgileTs +- The [`core`](https://agile-ts.org/docs/core) package contains the State Management Logic of AgileTs and therefore offers powerful classes such as the [`State Class`](https://agile-ts.org/docs/core/state). ``` npm install @agile-ts/core ``` -- And on the other hand, a _fitting Integration_ for your preferred UI-Framework. - In my case, the [React Integration](https://www.npmjs.com/package/@agile-ts/react). - Check [here](https://agile-ts.org/docs/frameworks) if your desired Framework is supported, too. +- A _fitting Integration_ for the UI-Framework of your choice, on the other hand, + is an interface to the actual UI and provides useful functionalities + to bind States to UI-Components for reactivity. + I prefer React, so let's go with the [React Integration](https://www.npmjs.com/package/@agile-ts/react) for now. ``` npm install @agile-ts/react ``` @@ -158,10 +156,11 @@ In order to properly use AgileTs, in a UI-Framework, we need to install **two**
Documentation -Sounds AgileTs interesting to you? -Checkout our **[documentation](https://agile-ts.org/docs/introduction)**, to learn more. -And I promise you. You will be able to use AgileTs in no time. -If you have any further questions, don't hesitate to join our [Community Discord](https://discord.gg/T9GzreAwPH). +Does AgileTs sound interesting to you? +Take a look at our **[documentation](https://agile-ts.org/docs/introduction)**, +to learn more about its functionalities and how it works exactly. +If you have any further questions, +don't hesitate to join our [Community Discord](https://discord.gg/T9GzreAwPH).
@@ -184,17 +183,17 @@ To find out more about contributing, check out the [CONTRIBUTING.md](https://git
Packages of Agile -| Name | Latest Version | Description | -| ------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- | -| [@agile-ts/core](/packages/core) | [![badge](https://img.shields.io/npm/v/@agile-ts/core.svg?style=flat-square)](https://www.npmjs.com/package/@agile-ts/core) | State Manager | -| [@agile-ts/react](/packages/react) | [![badge](https://img.shields.io/npm/v/@agile-ts/react.svg?style=flat-square)](https://www.npmjs.com/package/@agile-ts/react) | React Integration | -| [@agile-ts/vue](/packages/vue) | [![badge](https://img.shields.io/npm/v/@agile-ts/vue.svg?style=flat-square)](https://www.npmjs.com/package/@agile-ts/vue) | Vue Integration | -| [@agile-ts/api](/packages/api) | [![badge](https://img.shields.io/npm/v/@agile-ts/api.svg?style=flat-square)](https://www.npmjs.com/package/@agile-ts/api) | Promise based API | -| [@agile-ts/multieditor](/packages/multieditor) | [![badge](https://img.shields.io/npm/v/@agile-ts/multieditor.svg?style=flat-square)](https://www.npmjs.com/package/@agile-ts/multieditor) | Simple Form Manager | -| [@agile-ts/event](/packages/event) | [![badge](https://img.shields.io/npm/v/@agile-ts/event.svg?style=flat-square)](https://www.npmjs.com/package/@agile-ts/event) | Handy class for emitting UI Events | -| [@agile-ts/logger](/packages/logger) | [![badge](https://img.shields.io/npm/v/@agile-ts/logger.svg?style=flat-square)](https://www.npmjs.com/package/@agile-ts/logger) | Manages the logging of AgileTs | -| [@agile-ts/utils](/packages/utils) | [![badge](https://img.shields.io/npm/v/@agile-ts/utils.svg?style=flat-square)](https://www.npmjs.com/package/@agile-ts/utils) | Util functions of AgileTs | -| [@agile-ts/proxytree](/packages/proxytree) | [![badge](https://img.shields.io/npm/v/@agile-ts/proxytree.svg?style=flat-square)](https://www.npmjs.com/package/@agile-ts/proxytree) | Create Proxy Tree | +| Name | Latest Version | Description | +| ------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- | +| [@agile-ts/core](/packages/core) | [![badge](https://img.shields.io/npm/v/@agile-ts/core.svg?style=flat-square)](https://www.npmjs.com/package/@agile-ts/core) | State Manager Logic | +| [@agile-ts/react](/packages/react) | [![badge](https://img.shields.io/npm/v/@agile-ts/react.svg?style=flat-square)](https://www.npmjs.com/package/@agile-ts/react) | React Integration | +| [@agile-ts/vue](/packages/vue) | [![badge](https://img.shields.io/npm/v/@agile-ts/vue.svg?style=flat-square)](https://www.npmjs.com/package/@agile-ts/vue) | Vue Integration | +| [@agile-ts/api](/packages/api) | [![badge](https://img.shields.io/npm/v/@agile-ts/api.svg?style=flat-square)](https://www.npmjs.com/package/@agile-ts/api) | Promise based API | +| [@agile-ts/multieditor](/packages/multieditor) | [![badge](https://img.shields.io/npm/v/@agile-ts/multieditor.svg?style=flat-square)](https://www.npmjs.com/package/@agile-ts/multieditor) | Simple Form Manager | +| [@agile-ts/event](/packages/event) | [![badge](https://img.shields.io/npm/v/@agile-ts/event.svg?style=flat-square)](https://www.npmjs.com/package/@agile-ts/event) | Handy class for emitting UI Events | +| [@agile-ts/logger](/packages/logger) | [![badge](https://img.shields.io/npm/v/@agile-ts/logger.svg?style=flat-square)](https://www.npmjs.com/package/@agile-ts/logger) | Logging API of AgileTs | +| [@agile-ts/utils](/packages/utils) | [![badge](https://img.shields.io/npm/v/@agile-ts/utils.svg?style=flat-square)](https://www.npmjs.com/package/@agile-ts/utils) | Utilities of AgileTs | +| [@agile-ts/proxytree](/packages/proxytree) | [![badge](https://img.shields.io/npm/v/@agile-ts/proxytree.svg?style=flat-square)](https://www.npmjs.com/package/@agile-ts/proxytree) | Proxy Tree for tracking accessed properties | |
@@ -202,4 +201,6 @@ To find out more about contributing, check out the [CONTRIBUTING.md](https://git
Credits -AgileTs is inspired by [MVVM Frameworks](https://de.wikipedia.org/wiki/Model_View_ViewModel) like [MobX](https://mobx.js.org/README.html) and [PulseJs](https://github.com/pulse-framework/pulse). +AgileTs is inspired by [MVVM Frameworks](https://de.wikipedia.org/wiki/Model_View_ViewModel) +like [MobX](https://mobx.js.org/README.html) and [PulseJs](https://github.com/pulse-framework/pulse). +For the API, we were mainly inspired by [Svelte](https://svelte.dev/). diff --git a/jest.base.config.js b/jest.base.config.js index 33c9a7b1..9c42b9e0 100644 --- a/jest.base.config.js +++ b/jest.base.config.js @@ -15,5 +15,6 @@ module.exports = { 'ts-jest': { tsconfig: '/packages/tsconfig.default.json', }, + __DEV__: true, }, }; diff --git a/packages/core/src/agile.ts b/packages/core/src/agile.ts index adf0419c..9791ae40 100644 --- a/packages/core/src/agile.ts +++ b/packages/core/src/agile.ts @@ -22,11 +22,19 @@ import { DependableAgileInstancesType, CreateComputedConfigInterface, ComputeFunctionType, + createStorage, + createState, + createCollection, + createComputed, + IntegrationsConfigInterface, } from './internal'; export class Agile { public config: AgileConfigInterface; + // Key/Name identifier of Agile Instance + public key?: AgileKey; + // Queues and executes incoming Observer-based Jobs public runtime: Runtime; // Manages and simplifies the subscription to UI-Components @@ -34,12 +42,10 @@ export class Agile { // Handles the permanent persistence of Agile Classes public storages: Storages; - // Integrations (UI-Frameworks) that are integrated into AgileTs + // Integrations (UI-Frameworks) that are integrated into the Agile Instance public integrations: Integrations; - // External added Integrations that are to integrate into AgileTs when it is instantiated - static initialIntegrations: Integration[] = []; - // Static AgileTs Logger with the default config + // Static Agile Logger with the default config // (-> is overwritten by the last created Agile Instance) static logger = new Logger({ prefix: 'Agile', @@ -82,18 +88,15 @@ export class Agile { waitForMount: true, logConfig: {}, bindGlobal: false, - }); - config.logConfig = defineConfig(config.logConfig, { - prefix: 'Agile', - active: true, - level: Logger.level.WARN, - canUseCustomStyles: true, - allowedTags: ['runtime', 'storage', 'subscription', 'multieditor'], + autoIntegrate: true, }); this.config = { waitForMount: config.waitForMount as any, }; - this.integrations = new Integrations(this); + this.key = config.key; + this.integrations = new Integrations(this, { + autoIntegrate: config.autoIntegrate, + }); this.runtime = new Runtime(this); this.subController = new SubController(this); this.storages = new Storages(this, { @@ -101,7 +104,7 @@ export class Agile { }); // Assign customized Logger config to the static Logger - Agile.logger = new Logger(config.logConfig); + this.configureLogger(config.logConfig); LogCodeManager.log('10:00:00', [], this, Agile.logger); @@ -109,7 +112,27 @@ export class Agile { // Why? 'getAgileInstance()' returns the global Agile Instance // if it couldn't find any Agile Instance in the specified Instance. if (config.bindGlobal) - if (!globalBind(Agile.globalKey, this)) LogCodeManager.log('10:02:00'); + if (!globalBind(Agile.globalKey, this)) { + LogCodeManager.log('10:02:00'); + } + } + + /** + * Configures the logging behaviour of AgileTs. + * + * @public + * @param config - Configuration object + */ + public configureLogger(config: CreateLoggerConfigInterface = {}): this { + config = defineConfig(config, { + prefix: 'Agile', + active: true, + level: Logger.level.SUCCESS, + canUseCustomStyles: true, + allowedTags: ['runtime', 'storage', 'subscription', 'multieditor'], + }); + Agile.logger = new Logger(config); + return this; } /** @@ -128,7 +151,7 @@ export class Agile { * @param config - Configuration object */ public createStorage(config: CreateStorageConfigInterface): Storage { - return new Storage(config); + return createStorage(config); } /** @@ -150,7 +173,10 @@ export class Agile { initialValue: ValueType, config: StateConfigInterface = {} ): State { - return new State(this, initialValue, config); + return createState(initialValue, { + ...config, + ...{ agileInstance: this }, + }); } /** @@ -174,7 +200,7 @@ export class Agile { public createCollection( config?: CollectionConfig ): Collection { - return new Collection(this, config); + return createCollection(config, this); } /** @@ -232,12 +258,14 @@ export class Agile { if (Array.isArray(configOrDeps)) { _config = flatMerge(_config, { computedDeps: configOrDeps, + agileInstance: this, }); } else { - if (configOrDeps) _config = configOrDeps; + if (configOrDeps) + _config = { ...configOrDeps, ...{ agileInstance: this } }; } - return new Computed(this, computeFunction, _config); + return createComputed(computeFunction, _config); } /** @@ -303,7 +331,10 @@ export class Agile { } } -export interface CreateAgileConfigInterface { +export type AgileKey = string | number; + +export interface CreateAgileConfigInterface + extends IntegrationsConfigInterface { /** * Configures the logging behaviour of AgileTs. * @default { @@ -332,6 +363,11 @@ export interface CreateAgileConfigInterface { * @default false */ bindGlobal?: boolean; + /** + * Key/Name identifier of the Agile Instance. + * @default undefined + */ + key?: AgileKey; } export interface AgileConfigInterface { diff --git a/packages/core/src/integrations/index.ts b/packages/core/src/integrations/index.ts index 038166db..1efb0805 100644 --- a/packages/core/src/integrations/index.ts +++ b/packages/core/src/integrations/index.ts @@ -1,4 +1,8 @@ -import { Agile, Integration, LogCodeManager } from '../internal'; +import { Agile, defineConfig, Integration, LogCodeManager } from '../internal'; + +const onRegisterInitialIntegrationCallbacks: (( + integration: Integration +) => void)[] = []; export class Integrations { // Agile Instance the Integrations belongs to @@ -7,6 +11,39 @@ export class Integrations { // Registered Integrations public integrations: Set = new Set(); + // External added Integrations + // that are to integrate into each created Agile Instance + static initialIntegrations: Integration[] = []; + + /** + * Adds an external Integration to be registered in each Agile Instance created. + * + * @public + * @param integration - Integration to be registered in each Agile Instance created. + */ + static addInitialIntegration(integration: Integration): void { + if (integration instanceof Integration) { + // Executed external registered Integration callbacks + onRegisterInitialIntegrationCallbacks.forEach((callback) => + callback(integration) + ); + + Integrations.initialIntegrations.push(integration); + } + } + + /** + * Fires on each external added Integration. + * + * @public + * @param callback - Callback to be fired when an Integration was added externally. + */ + static onRegisterInitialIntegration( + callback: (integration: Integration) => void + ): void { + onRegisterInitialIntegrationCallbacks.push(callback); + } + /** * The Integrations Class manages all Integrations for an Agile Instance * and provides an interface to easily update @@ -14,14 +51,25 @@ export class Integrations { * * @internal * @param agileInstance - Instance of Agile the Integrations belongs to. + * @param config - Configuration object */ - constructor(agileInstance: Agile) { + constructor(agileInstance: Agile, config: IntegrationsConfigInterface = {}) { + config = defineConfig(config, { + autoIntegrate: true, + }); this.agileInstance = () => agileInstance; - // Integrate initial Integrations which were statically set externally - Agile.initialIntegrations.forEach((integration) => - this.integrate(integration) - ); + if (config.autoIntegrate) { + // Integrate Integrations to be initially integrated + Integrations.initialIntegrations.forEach((integration) => { + this.integrate(integration); + }); + + // Setup listener to be notified when an external registered Integration was added + Integrations.onRegisterInitialIntegration((integration) => { + this.integrate(integration); + }); + } } /** @@ -33,7 +81,7 @@ export class Integrations { */ public async integrate(integration: Integration): Promise { // Check if Integration is valid - if (!integration._key) { + if (integration._key == null) { LogCodeManager.log('18:03:00', [integration._key], integration); return false; } @@ -84,3 +132,14 @@ export class Integrations { return this.integrations.size > 0; } } + +export interface IntegrationsConfigInterface { + /** + * Whether external added Integrations + * are to integrate automatically into the Integrations Class. + * For example, when the package '@agile-ts/react' was installed, + * whether to automatically integrate the 'reactIntegration'. + * @default true + */ + autoIntegrate?: boolean; +} diff --git a/packages/core/src/internal.ts b/packages/core/src/internal.ts index 6e403e5a..ad6631d3 100644 --- a/packages/core/src/internal.ts +++ b/packages/core/src/internal.ts @@ -4,17 +4,21 @@ // !! All internal Agile modules must be imported from here!! -// Logger -export * from '@agile-ts/logger'; -export * from './logCodeManager'; - // Utils export * from './utils'; export * from '@agile-ts/utils'; +// Logger +export * from '@agile-ts/logger'; +export * from './logCodeManager'; + // Agile export * from './agile'; +// Integrations +export * from './integrations'; +export * from './integrations/integration'; + // Runtime export * from './runtime'; export * from './runtime/observer'; @@ -47,6 +51,5 @@ export * from './collection/item'; export * from './collection/selector'; export * from './collection/collection.persistent'; -// Integrations -export * from './integrations'; -export * from './integrations/integration'; +// Shared +export * from './shared'; diff --git a/packages/core/src/logCodeManager.ts b/packages/core/src/logCodeManager.ts index 36a1eba0..d90deb35 100644 --- a/packages/core/src/logCodeManager.ts +++ b/packages/core/src/logCodeManager.ts @@ -33,7 +33,7 @@ const logCodeMessages = { // Storages '11:02:00': - "The 'Local Storage' is not available in your current environment." + + "The 'Local Storage' is not available in your current environment. " + "To use the '.persist()' functionality, please provide a custom Storage!", '11:02:01': 'The first allocated Storage for AgileTs must be set as the default Storage!', diff --git a/packages/core/src/shared.ts b/packages/core/src/shared.ts new file mode 100644 index 00000000..d433eaab --- /dev/null +++ b/packages/core/src/shared.ts @@ -0,0 +1,200 @@ +import { + Agile, + Collection, + CollectionConfig, + Computed, + ComputeFunctionType, + CreateComputedConfigInterface, + CreateStorageConfigInterface, + DefaultItem, + defineConfig, + DependableAgileInstancesType, + flatMerge, + Logger, + removeProperties, + runsOnServer, + State, + StateConfigInterface, + Storage, +} from './internal'; + +/** + * Shared Agile Instance that is used when no Agile Instance was specified. + */ +let sharedAgileInstance = new Agile({ + key: 'shared', + logConfig: { prefix: 'Agile', level: Logger.level.WARN, active: true }, + localStorage: !runsOnServer(), +}); +export { sharedAgileInstance as shared }; + +/** + * Assigns the specified Agile Instance as the shared Agile Instance. + * + * @param agileInstance - Agile Instance to become the new shared Agile Instance. + */ +// https://stackoverflow.com/questions/32558514/javascript-es6-export-const-vs-export-let +export function assignSharedAgileInstance(agileInstance: Agile): void { + sharedAgileInstance = agileInstance; +} + +/** + * Returns a newly created Storage. + * + * A Storage Class serves as an interface to external storages, + * such as the [Async Storage](https://github.com/react-native-async-storage/async-storage) or + * [Local Storage](https://www.w3schools.com/html/html5_webstorage.asp). + * + * It creates the foundation to easily [`persist()`](https://agile-ts.org/docs/core/state/methods#persist) [Agile Sub Instances](https://agile-ts.org/docs/introduction/#agile-sub-instance) + * (like States or Collections) in nearly any external storage. + * + * [Learn more..](https://agile-ts.org/docs/core/agile-instance/methods#createstorage) + * + * @public + * @param config - Configuration object + */ +export function createStorage(config: CreateStorageConfigInterface): Storage { + return new Storage(config); +} + +/** + * Returns a newly created State. + * + * A State manages a piece of Information + * that we need to remember globally at a later point in time. + * While providing a toolkit to use and mutate this piece of Information. + * + * You can create as many global States as you need. + * + * [Learn more..](https://agile-ts.org/docs/core/agile-instance/methods#createstate) + * + * @public + * @param initialValue - Initial value of the State. + * @param config - Configuration object + */ +export function createState( + initialValue: ValueType, + config: CreateStateConfigInterfaceWithAgile = {} +): State { + config = defineConfig(config, { + agileInstance: sharedAgileInstance, + }); + return new State( + config.agileInstance as any, + initialValue, + removeProperties(config, ['agileInstance']) + ); +} + +/** + * Returns a newly created Collection. + * + * A Collection manages a reactive set of Information + * that we need to remember globally at a later point in time. + * While providing a toolkit to use and mutate this set of Information. + * + * It is designed for arrays of data objects following the same pattern. + * + * Each of these data object must have a unique `primaryKey` to be correctly identified later. + * + * You can create as many global Collections as you need. + * + * [Learn more..](https://agile-ts.org/docs/core/agile-instance/methods#createcollection) + * + * @public + * @param config - Configuration object + * @param agileInstance - Instance of Agile the Collection belongs to. + */ +export function createCollection( + config?: CollectionConfig, + agileInstance: Agile = sharedAgileInstance +): Collection { + return new Collection(agileInstance, config); +} + +/** + * Returns a newly created Computed. + * + * A Computed is an extension of the State Class + * that computes its value based on a specified compute function. + * + * The computed value will be cached to avoid unnecessary recomputes + * and is only recomputed when one of its direct dependencies changes. + * + * Direct dependencies can be States and Collections. + * So when, for example, a dependent State value changes, the computed value is recomputed. + * + * [Learn more..](https://agile-ts.org/docs/core/agile-instance/methods#createstate) + * + * @public + * @param computeFunction - Function to compute the computed value. + * @param config - Configuration object + */ +export function createComputed( + computeFunction: ComputeFunctionType, + config?: CreateComputedConfigInterfaceWithAgile +): Computed; +/** + * Returns a newly created Computed. + * + * A Computed is an extension of the State Class + * that computes its value based on a specified compute function. + * + * The computed value will be cached to avoid unnecessary recomputes + * and is only recomputed when one of its direct dependencies changes. + * + * Direct dependencies can be States and Collections. + * So when, for example, a dependent State value changes, the computed value is recomputed. + * + * [Learn more..](https://agile-ts.org/docs/core/agile-instance/methods#createcomputed) + * + * @public + * @param computeFunction - Function to compute the computed value. + * @param deps - Hard-coded dependencies on which the Computed Class should depend. + */ +export function createComputed( + computeFunction: ComputeFunctionType, + deps?: Array +): Computed; +export function createComputed( + computeFunction: ComputeFunctionType, + configOrDeps?: + | CreateComputedConfigInterface + | Array +): Computed { + let _config: CreateComputedConfigInterfaceWithAgile = {}; + + if (Array.isArray(configOrDeps)) { + _config = flatMerge(_config, { + computedDeps: configOrDeps, + }); + } else { + if (configOrDeps) _config = configOrDeps; + } + + _config = defineConfig(_config, { + agileInstance: sharedAgileInstance, + }); + + return new Computed( + _config.agileInstance as any, + computeFunction, + removeProperties(_config, ['agileInstance']) + ); +} + +export interface CreateAgileSubInstanceInterface { + /** + * Instance of Agile the Instance belongs to. + * @default Agile.shared + */ + agileInstance?: Agile; +} + +export interface CreateStateConfigInterfaceWithAgile + extends CreateAgileSubInstanceInterface, + StateConfigInterface {} + +export interface CreateComputedConfigInterfaceWithAgile + extends CreateAgileSubInstanceInterface, + CreateComputedConfigInterface {} diff --git a/packages/core/src/utils.ts b/packages/core/src/utils.ts index 56af1728..8748f28e 100644 --- a/packages/core/src/utils.ts +++ b/packages/core/src/utils.ts @@ -5,6 +5,7 @@ import { normalizeArray, isFunction, LogCodeManager, + shared, } from './internal'; /** @@ -25,6 +26,11 @@ export function getAgileInstance(instance: any): Agile | undefined { if (_agileInstance) return _agileInstance; } + // Try to get shared Agile Instance + if (shared instanceof Agile) { + return shared; + } + // Return global bound Agile Instance return globalThis[Agile.globalKey]; } catch (e) { @@ -254,3 +260,16 @@ export function globalBind( } return false; } + +/** + * Returns a boolean indicating whether AgileTs is currently running on a server. + * + * @public + */ +export const runsOnServer = (): boolean => { + return !( + typeof window !== 'undefined' && + typeof window.document !== 'undefined' && + typeof window.document.createElement !== 'undefined' + ); +}; diff --git a/packages/core/tests/helper/logMock.ts b/packages/core/tests/helper/logMock.ts index 49c4c4dc..b431beb8 100644 --- a/packages/core/tests/helper/logMock.ts +++ b/packages/core/tests/helper/logMock.ts @@ -22,7 +22,7 @@ const logTypes = { }; function mockLogs(mockArg?: LogTypes[]): void { - const _mockArg = mockArg ?? ['warn', 'error']; + const _mockArg = mockArg ?? ['warn', 'error', 'log']; mockConsole(_mockArg); } diff --git a/packages/core/tests/integration/collection.persistent.integration.test.ts b/packages/core/tests/integration/collection.persistent.integration.test.ts index 203fbfd4..e1d151ee 100644 --- a/packages/core/tests/integration/collection.persistent.integration.test.ts +++ b/packages/core/tests/integration/collection.persistent.integration.test.ts @@ -17,16 +17,7 @@ describe('Collection Persist Function Tests', () => { delete myStorage[key]; }), }; - - // Define Agile with Storage - const App = new Agile({ localStorage: false }); - App.registerStorage( - App.createStorage({ - key: 'testStorage', - prefix: 'test', - methods: storageMethods, - }) - ); + let App: Agile; interface User { id: number; @@ -36,6 +27,15 @@ describe('Collection Persist Function Tests', () => { beforeEach(() => { LogMock.mockLogs(); jest.clearAllMocks(); + + App = new Agile({ localStorage: false }); + App.registerStorage( + App.createStorage({ + key: 'testStorage', + prefix: 'test', + methods: storageMethods, + }) + ); }); describe('Collection', () => { diff --git a/packages/core/tests/unit/agile.test.ts b/packages/core/tests/unit/agile.test.ts index f1bdec49..02ae2ef7 100644 --- a/packages/core/tests/unit/agile.test.ts +++ b/packages/core/tests/unit/agile.test.ts @@ -12,32 +12,48 @@ import { } from '../../src'; import testIntegration from '../helper/test.integration'; import { LogMock } from '../helper/logMock'; +import * as Shared from '../../src/shared'; -jest.mock('../../src/runtime/index'); -jest.mock('../../src/runtime/subscription/sub.controller'); -jest.mock('../../src/storages/index'); -jest.mock('../../src/integrations/index'); -jest.mock('../../src/storages/storage'); -jest.mock('../../src/collection/index'); -jest.mock('../../src/computed/index'); -/* Can't mock Logger because I somehow can't overwrite a static get method -jest.mock("../../src/logger/index", () => { - return class { - static get level() { +// https://github.com/facebook/jest/issues/5023 +jest.mock('../../src/runtime', () => { + return { + // https://jestjs.io/docs/mock-function-api#mockfnmockimplementationfn + Runtime: jest.fn().mockImplementation(() => { return { - TRACE: 1, - DEBUG: 2, - LOG: 5, - TABLE: 5, - INFO: 10, - WARN: 20, - ERROR: 50, + ingest: jest.fn(), }; - } + }), + }; +}); +jest.mock('../../src/runtime/subscription/sub.controller', () => { + return { + SubController: jest.fn(), + }; +}); +jest.mock('../../src/storages', () => { + return { + Storages: jest.fn(), + }; +}); + +// https://gist.github.com/virgs/d9c50e878fc69832c01f8085f2953f12 +// https://medium.com/@masonlgoetz/mock-static-class-methods-in-jest-1ceda967b47f +jest.mock('../../src/integrations', () => { + const mockedInstances = { + // https://jestjs.io/docs/mock-function-api#mockfnmockimplementationfn + Integrations: jest.fn().mockImplementation(() => { + return { + integrate: jest.fn(), + hasIntegration: jest.fn(), + }; + }), }; + // @ts-ignore + mockedInstances.Integrations.onRegisteredExternalIntegration = jest.fn(); + // @ts-ignore + mockedInstances.Integrations.initialIntegrations = []; + return mockedInstances; }); - */ -// jest.mock("../../src/state/index"); // Can't mock State because mocks get instantiated before everything else -> I got the good old not loaded Object error https://github.com/kentcdodds/how-jest-mocking-works describe('Agile Tests', () => { const RuntimeMock = Runtime as jest.MockedClass; @@ -50,56 +66,49 @@ describe('Agile Tests', () => { >; beforeEach(() => { - jest.clearAllMocks(); LogMock.mockLogs(); + // Clear specified mocks RuntimeMock.mockClear(); SubControllerMock.mockClear(); StoragesMock.mockClear(); IntegrationsMock.mockClear(); - // Reset Global This + // Reset globalThis globalThis[Agile.globalKey] = undefined; + + jest.spyOn(Agile.prototype, 'configureLogger'); + jest.spyOn(Agile.prototype, 'integrate'); + + jest.clearAllMocks(); }); it('should instantiate Agile (default config)', () => { const agile = new Agile(); - // Check if Agile properties got instantiated properly expect(agile.config).toStrictEqual({ waitForMount: true, }); - expect(IntegrationsMock).toHaveBeenCalledWith(agile); - expect(agile.integrations).toBeInstanceOf(Integrations); + expect(agile.key).toBeUndefined(); + expect(IntegrationsMock).toHaveBeenCalledWith(agile, { + autoIntegrate: true, + }); + // expect(agile.integrations).toBeInstanceOf(Integrations); // Because 'Integrations' is completely overwritten with a mock (mockImplementation) expect(RuntimeMock).toHaveBeenCalledWith(agile); - expect(agile.runtime).toBeInstanceOf(Runtime); + // expect(agile.runtime).toBeInstanceOf(Runtime); // Because 'Runtime' is completely overwritten with a mock (mockImplementation) expect(SubControllerMock).toHaveBeenCalledWith(agile); expect(agile.subController).toBeInstanceOf(SubController); expect(StoragesMock).toHaveBeenCalledWith(agile, { localStorage: true, }); expect(agile.storages).toBeInstanceOf(Storages); + expect(agile.configureLogger).toHaveBeenCalledWith({}); - // Check if Static Logger has correct config - expect(Agile.logger.config).toStrictEqual({ - prefix: 'Agile', - level: Logger.level.WARN, - canUseCustomStyles: true, - timestamp: false, - }); - expect(Agile.logger.allowedTags).toStrictEqual([ - 'runtime', - 'storage', - 'subscription', - 'multieditor', - ]); - expect(Agile.logger.isActive).toBeTruthy(); - - // Check if global Agile Instance got created + // Check if Agile Instance got bound globally expect(globalThis[Agile.globalKey]).toBeUndefined(); }); - it('should instantiate Agile with (specific config)', () => { + it('should instantiate Agile (specific config)', () => { const agile = new Agile({ waitForMount: false, localStorage: false, @@ -110,71 +119,97 @@ describe('Agile Tests', () => { timestamp: true, }, bindGlobal: true, + key: 'jeff', + autoIntegrate: false, }); - // Check if Agile properties got instantiated properly expect(agile.config).toStrictEqual({ waitForMount: false, }); - expect(IntegrationsMock).toHaveBeenCalledWith(agile); - expect(agile.integrations).toBeInstanceOf(Integrations); + expect(agile.key).toBe('jeff'); + expect(IntegrationsMock).toHaveBeenCalledWith(agile, { + autoIntegrate: false, + }); + // expect(agile.integrations).toBeInstanceOf(Integrations); // Because 'Integrations' is completely overwritten with a mock (mockImplementation) expect(RuntimeMock).toHaveBeenCalledWith(agile); - expect(agile.runtime).toBeInstanceOf(Runtime); + // expect(agile.runtime).toBeInstanceOf(Runtime); // Because 'Runtime' is completely overwritten with a mock (mockImplementation) expect(SubControllerMock).toHaveBeenCalledWith(agile); expect(agile.subController).toBeInstanceOf(SubController); expect(StoragesMock).toHaveBeenCalledWith(agile, { localStorage: false, }); expect(agile.storages).toBeInstanceOf(Storages); - - // Check if Static Logger has correct config - expect(Agile.logger.config).toStrictEqual({ - prefix: 'Jeff', + expect(agile.configureLogger).toHaveBeenCalledWith({ + active: false, level: Logger.level.DEBUG, - canUseCustomStyles: true, + prefix: 'Jeff', timestamp: true, }); - expect(Agile.logger.allowedTags).toStrictEqual([ - 'runtime', - 'storage', - 'subscription', - 'multieditor', - ]); - expect(Agile.logger.isActive).toBeFalsy(); - - // Check if global Agile Instance got created + + // Check if Agile Instance got bound globally expect(globalThis[Agile.globalKey]).toBe(agile); }); - it('should instantiate second Agile Instance and print warning if config.bindGlobal is set both times to true', () => { - const agile1 = new Agile({ - bindGlobal: true, - }); + it( + 'should instantiate second Agile Instance ' + + 'and print warning when an attempt is made to set the second Agile Instance globally ' + + 'although the previously defined Agile Instance is already globally set', + () => { + const agile1 = new Agile({ + bindGlobal: true, + }); - const agile2 = new Agile({ - bindGlobal: true, - }); + const agile2 = new Agile({ + bindGlobal: true, + }); - expect(globalThis[Agile.globalKey]).toBe(agile1); - LogMock.hasLoggedCode('10:02:00'); - }); + expect(agile1).toBeInstanceOf(Agile); + expect(agile2).toBeInstanceOf(Agile); + + expect(globalThis[Agile.globalKey]).toBe(agile1); + LogMock.hasLoggedCode('10:02:00'); + } + ); describe('Agile Function Tests', () => { let agile: Agile; beforeEach(() => { agile = new Agile(); - jest.clearAllMocks(); // Because creating Agile executes some mocks + jest.clearAllMocks(); // Because creating the Agile Instance calls some mocks }); - describe('createStorage function tests', () => { - const StorageMock = Storage as jest.MockedClass; + describe('configureLogger function tests', () => { + it('should overwrite the static Logger with a new Logger Instance', () => { + Agile.logger.config = 'outdated' as any; + agile.configureLogger({ + active: true, + level: 0, + }); + + expect(Agile.logger.config).toStrictEqual({ + canUseCustomStyles: true, + level: 0, + prefix: 'Agile', + timestamp: false, + }); + expect(Agile.logger.isActive).toBeTruthy(); + expect(Agile.logger.allowedTags).toStrictEqual([ + 'runtime', + 'storage', + 'subscription', + 'multieditor', + ]); + }); + }); + + describe('createStorage function tests', () => { beforeEach(() => { - StorageMock.mockClear(); + jest.spyOn(Shared, 'createStorage'); }); - it('should create Storage', () => { + it('should call createStorage', () => { const storageConfig = { prefix: 'test', methods: { @@ -190,31 +225,36 @@ describe('Agile Tests', () => { }, key: 'myTestStorage', }; - const storage = agile.createStorage(storageConfig); - expect(storage).toBeInstanceOf(Storage); - expect(StorageMock).toHaveBeenCalledWith(storageConfig); + const response = agile.createStorage(storageConfig); + + expect(response).toBeInstanceOf(Storage); + expect(Shared.createStorage).toHaveBeenCalledWith(storageConfig); }); }); - describe('state function tests', () => { - it('should create State', () => { - const state = agile.createState('testValue', { - key: 'myCoolState', - }); + describe('createState function tests', () => { + beforeEach(() => { + jest.spyOn(Shared, 'createState'); + }); - expect(state).toBeInstanceOf(State); + it('should call createState with the Agile Instance it was called on', () => { + const response = agile.createState('jeff', { key: 'jeffState' }); + + expect(response).toBeInstanceOf(State); + expect(Shared.createState).toHaveBeenCalledWith('jeff', { + key: 'jeffState', + agileInstance: agile, + }); }); }); describe('createCollection function tests', () => { - const CollectionMock = Collection as jest.MockedClass; - beforeEach(() => { - CollectionMock.mockClear(); + jest.spyOn(Shared, 'createCollection'); }); - it('should create Collection', () => { + it('should call createCollection with the Agile Instance it was called on', () => { const collectionConfig = { selectors: ['test', 'test1'], groups: ['test2', 'test10'], @@ -222,48 +262,53 @@ describe('Agile Tests', () => { key: 'myCoolCollection', }; - const collection = agile.createCollection(collectionConfig); + const response = agile.createCollection(collectionConfig); - expect(collection).toBeInstanceOf(Collection); - expect(CollectionMock).toHaveBeenCalledWith(agile, collectionConfig); + expect(response).toBeInstanceOf(Collection); + expect(Shared.createCollection).toHaveBeenCalledWith( + collectionConfig, + agile + ); }); }); describe('createComputed function tests', () => { - const ComputedMock = Computed as jest.MockedClass; const computedFunction = () => { - // console.log("Hello Jeff"); + // empty }; beforeEach(() => { - ComputedMock.mockClear(); + jest.spyOn(Shared, 'createComputed'); }); - it('should create Computed', () => { - const computed = agile.createComputed(computedFunction, [ + it('should call createComputed with the Agile Instance it was called on (default config)', () => { + const response = agile.createComputed(computedFunction, [ 'dummyDep' as any, ]); - expect(computed).toBeInstanceOf(Computed); - expect(ComputedMock).toHaveBeenCalledWith(agile, computedFunction, { + expect(response).toBeInstanceOf(Computed); + expect(Shared.createComputed).toHaveBeenCalledWith(computedFunction, { computedDeps: ['dummyDep' as any], + agileInstance: agile, }); }); - it('should create Computed with config', () => { - const computed = agile.createComputed(computedFunction, { + it('should call createComputed with the Agile Instance it was called on (specific config)', () => { + const computedConfig = { key: 'jeff', isPlaceholder: false, computedDeps: ['dummyDep' as any], autodetect: true, - }); + }; - expect(computed).toBeInstanceOf(Computed); - expect(ComputedMock).toHaveBeenCalledWith(agile, computedFunction, { - key: 'jeff', - isPlaceholder: false, - computedDeps: ['dummyDep' as any], - autodetect: true, + const response = agile.createComputed(computedFunction, computedConfig); + + expect(response).toBeInstanceOf(Computed); + expect(Shared.createComputed).toHaveBeenCalledWith(computedFunction, { + ...computedConfig, + ...{ + agileInstance: agile, + }, }); }); }); @@ -280,6 +325,10 @@ describe('Agile Tests', () => { }); describe('registerStorage function tests', () => { + beforeEach(() => { + agile.storages.register = jest.fn(); + }); + it('should register provided Storage', () => { const dummyStorage = new Storage({ prefix: 'test', @@ -317,6 +366,10 @@ describe('Agile Tests', () => { }); describe('hasStorage function tests', () => { + beforeEach(() => { + agile.storages.hasStorage = jest.fn(); + }); + it('should check if Agile has any registered Storage', () => { agile.hasStorage(); diff --git a/packages/core/tests/unit/collection/collection.persistent.test.ts b/packages/core/tests/unit/collection/collection.persistent.test.ts index 3ebbc97c..0c746a27 100644 --- a/packages/core/tests/unit/collection/collection.persistent.test.ts +++ b/packages/core/tests/unit/collection/collection.persistent.test.ts @@ -20,7 +20,6 @@ describe('CollectionPersistent Tests', () => { let dummyCollection: Collection; beforeEach(() => { - jest.clearAllMocks(); LogMock.mockLogs(); dummyAgile = new Agile({ localStorage: false }); @@ -30,6 +29,8 @@ describe('CollectionPersistent Tests', () => { jest.spyOn(CollectionPersistent.prototype, 'instantiatePersistent'); jest.spyOn(CollectionPersistent.prototype, 'initialLoading'); + + jest.clearAllMocks(); }); it('should create CollectionPersistent and should call initialLoading if Persistent is ready (default config)', () => { diff --git a/packages/core/tests/unit/collection/collection.test.ts b/packages/core/tests/unit/collection/collection.test.ts index 47db19a4..e5b0e686 100644 --- a/packages/core/tests/unit/collection/collection.test.ts +++ b/packages/core/tests/unit/collection/collection.test.ts @@ -22,7 +22,6 @@ describe('Collection Tests', () => { let dummyAgile: Agile; beforeEach(() => { - jest.clearAllMocks(); LogMock.mockLogs(); dummyAgile = new Agile({ localStorage: false }); @@ -30,6 +29,8 @@ describe('Collection Tests', () => { jest.spyOn(Collection.prototype, 'initSelectors'); jest.spyOn(Collection.prototype, 'initGroups'); jest.spyOn(Collection.prototype, 'collect'); + + jest.clearAllMocks(); }); it('should create Collection (default config)', () => { diff --git a/packages/core/tests/unit/collection/group/group.observer.test.ts b/packages/core/tests/unit/collection/group/group.observer.test.ts index f24e14d6..a295262e 100644 --- a/packages/core/tests/unit/collection/group/group.observer.test.ts +++ b/packages/core/tests/unit/collection/group/group.observer.test.ts @@ -24,7 +24,6 @@ describe('GroupObserver Tests', () => { let dummyItem2: Item; beforeEach(() => { - jest.clearAllMocks(); LogMock.mockLogs(); dummyAgile = new Agile({ localStorage: false }); @@ -40,6 +39,8 @@ describe('GroupObserver Tests', () => { id: 'dummyItem2Key', name: 'jeff', }); + + jest.clearAllMocks(); }); it('should create Group Observer (default config)', () => { diff --git a/packages/core/tests/unit/collection/group/group.test.ts b/packages/core/tests/unit/collection/group/group.test.ts index 7aaceb4f..f9d48a74 100644 --- a/packages/core/tests/unit/collection/group/group.test.ts +++ b/packages/core/tests/unit/collection/group/group.test.ts @@ -21,7 +21,6 @@ describe('Group Tests', () => { let dummyCollection: Collection; beforeEach(() => { - jest.clearAllMocks(); LogMock.mockLogs(); dummyAgile = new Agile({ localStorage: false }); @@ -31,6 +30,8 @@ describe('Group Tests', () => { jest.spyOn(Group.prototype, 'rebuild'); jest.spyOn(Group.prototype, 'addSideEffect'); + + jest.clearAllMocks(); }); it('should create Group with no initialItems (default config)', () => { diff --git a/packages/core/tests/unit/collection/item.test.ts b/packages/core/tests/unit/collection/item.test.ts index b50b620a..374352b8 100644 --- a/packages/core/tests/unit/collection/item.test.ts +++ b/packages/core/tests/unit/collection/item.test.ts @@ -18,13 +18,14 @@ describe('Item Tests', () => { let dummyCollection: Collection; beforeEach(() => { - jest.clearAllMocks(); LogMock.mockLogs(); dummyAgile = new Agile({ localStorage: false }); dummyCollection = new Collection(dummyAgile); jest.spyOn(Item.prototype, 'addRebuildGroupThatIncludeItemKeySideEffect'); + + jest.clearAllMocks(); }); it('should create Item (default config)', () => { diff --git a/packages/core/tests/unit/collection/selector.test.ts b/packages/core/tests/unit/collection/selector.test.ts index 092a0d4c..11080f9f 100644 --- a/packages/core/tests/unit/collection/selector.test.ts +++ b/packages/core/tests/unit/collection/selector.test.ts @@ -11,13 +11,14 @@ describe('Selector Tests', () => { let dummyCollection: Collection; beforeEach(() => { - jest.clearAllMocks(); LogMock.mockLogs(); dummyAgile = new Agile({ localStorage: false }); dummyCollection = new Collection(dummyAgile); jest.spyOn(Selector.prototype, 'select'); + + jest.clearAllMocks(); }); it('should create Selector and call initial select (default config)', () => { diff --git a/packages/core/tests/unit/computed/computed.test.ts b/packages/core/tests/unit/computed/computed.test.ts index 7f4c58fe..ef88decc 100644 --- a/packages/core/tests/unit/computed/computed.test.ts +++ b/packages/core/tests/unit/computed/computed.test.ts @@ -14,13 +14,14 @@ describe('Computed Tests', () => { let dummyAgile: Agile; beforeEach(() => { - jest.clearAllMocks(); LogMock.mockLogs(); dummyAgile = new Agile({ localStorage: false }); jest.spyOn(Computed.prototype, 'recompute'); jest.spyOn(Utils, 'extractRelevantObservers'); + + jest.clearAllMocks(); }); it('should create Computed with a not async compute method (default config)', () => { diff --git a/packages/core/tests/unit/computed/computed.tracker.test.ts b/packages/core/tests/unit/computed/computed.tracker.test.ts index 81483956..20e09dc1 100644 --- a/packages/core/tests/unit/computed/computed.tracker.test.ts +++ b/packages/core/tests/unit/computed/computed.tracker.test.ts @@ -5,7 +5,6 @@ describe('ComputedTracker Tests', () => { let dummyAgile: Agile; beforeEach(() => { - jest.clearAllMocks(); LogMock.mockLogs(); dummyAgile = new Agile({ localStorage: false }); @@ -13,6 +12,8 @@ describe('ComputedTracker Tests', () => { // Reset ComputedTracker (because it works static) ComputedTracker.isTracking = false; ComputedTracker.trackedObservers = new Set(); + + jest.clearAllMocks(); }); describe('ComputedTracker Function Tests', () => { diff --git a/packages/core/tests/unit/integrations/integration.test.ts b/packages/core/tests/unit/integrations/integration.test.ts index f9cac640..a73cc5c9 100644 --- a/packages/core/tests/unit/integrations/integration.test.ts +++ b/packages/core/tests/unit/integrations/integration.test.ts @@ -3,8 +3,8 @@ import { LogMock } from '../../helper/logMock'; describe('Integration Tests', () => { beforeEach(() => { - jest.clearAllMocks(); LogMock.mockLogs(); + jest.clearAllMocks(); }); it('should create Integration', () => { diff --git a/packages/core/tests/unit/integrations/integrations.test.ts b/packages/core/tests/unit/integrations/integrations.test.ts index d4baf660..b8550a22 100644 --- a/packages/core/tests/unit/integrations/integrations.test.ts +++ b/packages/core/tests/unit/integrations/integrations.test.ts @@ -3,56 +3,104 @@ import { LogMock } from '../../helper/logMock'; describe('Integrations Tests', () => { let dummyAgile: Agile; + let dummyIntegration1: Integration; + let dummyIntegration2: Integration; beforeEach(() => { - jest.clearAllMocks(); LogMock.mockLogs(); dummyAgile = new Agile({ localStorage: false }); - Agile.initialIntegrations = []; + dummyIntegration1 = new Integration({ + key: 'dummyIntegration1', + }); + dummyIntegration2 = new Integration({ + key: 'dummyIntegration2', + }); - jest.spyOn(Integrations.prototype, 'integrate'); - }); + Integrations.initialIntegrations = []; - it('should create Integrations', () => { - const integrations = new Integrations(dummyAgile); + jest.spyOn(Integrations.prototype, 'integrate'); + jest.spyOn(Integrations, 'onRegisterInitialIntegration'); - expect(integrations.integrations.size).toBe(0); + jest.clearAllMocks(); }); - it('should create Integrations and integrate Agile initialIntegrations', async () => { - const dummyIntegration1 = new Integration({ - key: 'initialIntegration1', - }); - const dummyIntegration2 = new Integration({ - key: 'initialIntegration2', - }); - Agile.initialIntegrations.push(dummyIntegration1); - Agile.initialIntegrations.push(dummyIntegration2); + it('should create Integrations with the before specified initial Integrations (default config)', () => { + Integrations.initialIntegrations = [dummyIntegration1, dummyIntegration2]; const integrations = new Integrations(dummyAgile); - expect(integrations.integrations.size).toBe(2); - expect(integrations.integrations.has(dummyIntegration1)).toBeTruthy(); - expect(integrations.integrations.has(dummyIntegration2)).toBeTruthy(); + expect(Array.from(integrations.integrations)).toStrictEqual([ + dummyIntegration1, + dummyIntegration2, + ]); + expect(Integrations.onRegisterInitialIntegration).toHaveBeenCalledWith( + expect.any(Function) + ); expect(integrations.integrate).toHaveBeenCalledWith(dummyIntegration1); expect(integrations.integrate).toHaveBeenCalledWith(dummyIntegration2); }); + it('should create Integrations without the before specified initial Integrations (specific config)', () => { + Integrations.initialIntegrations = [dummyIntegration1, dummyIntegration2]; + + const integrations = new Integrations(dummyAgile, { autoIntegrate: false }); + + expect(Array.from(integrations.integrations)).toStrictEqual([]); + + expect(Integrations.onRegisterInitialIntegration).not.toHaveBeenCalled(); + expect(integrations.integrate).not.toHaveBeenCalled(); + }); + describe('Integrations Function Tests', () => { let integrations: Integrations; - let dummyIntegration1: Integration; - let dummyIntegration2: Integration; beforeEach(() => { integrations = new Integrations(dummyAgile); - dummyIntegration1 = new Integration({ - key: 'dummyIntegration1', + }); + + describe('onRegisterInitialIntegration function tests', () => { + it('should register specified onRegisterInitialIntegration callback', () => { + // Nothing to testable }); - dummyIntegration2 = new Integration({ - key: 'dummyIntegration2', + }); + + describe('addInitialIntegration function tests', () => { + const callback1 = jest.fn(); + const callback2 = jest.fn(); + + beforeEach(() => { + Integrations.onRegisterInitialIntegration(callback1); + Integrations.onRegisterInitialIntegration(callback2); }); + it( + 'should add valid Integration to the initialIntegrations array ' + + 'and fire the onRegisterInitialIntegration callbacks', + () => { + Integrations.addInitialIntegration(dummyIntegration1); + + expect(callback1).toHaveBeenCalledTimes(1); + expect(callback1).toHaveBeenCalledWith(dummyIntegration1); + expect(callback2).toHaveBeenCalledTimes(1); + expect(callback2).toHaveBeenCalledWith(dummyIntegration1); + expect(Integrations.initialIntegrations).toStrictEqual([ + dummyIntegration1, + ]); + } + ); + + it( + "shouldn't add invalid Integration to the initialIntegrations array " + + "and shouldn't fire the onRegisterInitialIntegration callbacks", + () => { + Integrations.addInitialIntegration(undefined as any); + + expect(callback1).not.toHaveBeenCalled(); + expect(callback2).not.toHaveBeenCalled(); + expect(Integrations.initialIntegrations).toStrictEqual([]); + } + ); }); describe('integrate function tests', () => { diff --git a/packages/core/tests/unit/runtime/observer.test.ts b/packages/core/tests/unit/runtime/observer.test.ts index 59889d40..5810c138 100644 --- a/packages/core/tests/unit/runtime/observer.test.ts +++ b/packages/core/tests/unit/runtime/observer.test.ts @@ -15,7 +15,6 @@ describe('Observer Tests', () => { let dummySubscription2: SubscriptionContainer; beforeEach(() => { - jest.clearAllMocks(); LogMock.mockLogs(); dummyAgile = new Agile(); @@ -26,6 +25,8 @@ describe('Observer Tests', () => { jest.spyOn(dummySubscription1, 'addSubscription'); jest.spyOn(dummySubscription2, 'addSubscription'); + + jest.clearAllMocks(); }); it('should create Observer (default config)', () => { diff --git a/packages/core/tests/unit/runtime/runtime.job.test.ts b/packages/core/tests/unit/runtime/runtime.job.test.ts index 4793df21..b01c3765 100644 --- a/packages/core/tests/unit/runtime/runtime.job.test.ts +++ b/packages/core/tests/unit/runtime/runtime.job.test.ts @@ -7,7 +7,6 @@ describe('RuntimeJob Tests', () => { let dummyObserver: Observer; beforeEach(() => { - jest.clearAllMocks(); LogMock.mockLogs(); dummyAgile = new Agile({ localStorage: false }); @@ -15,6 +14,8 @@ describe('RuntimeJob Tests', () => { key: 'myIntegration', }); dummyObserver = new Observer(dummyAgile); + + jest.clearAllMocks(); }); it( diff --git a/packages/core/tests/unit/runtime/runtime.test.ts b/packages/core/tests/unit/runtime/runtime.test.ts index 2906607d..f72b5736 100644 --- a/packages/core/tests/unit/runtime/runtime.test.ts +++ b/packages/core/tests/unit/runtime/runtime.test.ts @@ -14,10 +14,11 @@ describe('Runtime Tests', () => { let dummyAgile: Agile; beforeEach(() => { - jest.clearAllMocks(); LogMock.mockLogs(); dummyAgile = new Agile({ localStorage: false }); + + jest.clearAllMocks(); }); it('should create Runtime', () => { diff --git a/packages/core/tests/unit/runtime/subscription/container/CallbackSubscriptionContainer.test.ts b/packages/core/tests/unit/runtime/subscription/container/CallbackSubscriptionContainer.test.ts index e8bea19e..7f342dc9 100644 --- a/packages/core/tests/unit/runtime/subscription/container/CallbackSubscriptionContainer.test.ts +++ b/packages/core/tests/unit/runtime/subscription/container/CallbackSubscriptionContainer.test.ts @@ -15,7 +15,6 @@ describe('CallbackSubscriptionContainer Tests', () => { let dummyProxyWeakMap: ProxyWeakMapType; beforeEach(() => { - jest.clearAllMocks(); LogMock.mockLogs(); dummyAgile = new Agile(); @@ -23,6 +22,8 @@ describe('CallbackSubscriptionContainer Tests', () => { dummyObserver2 = new Observer(dummyAgile, { key: 'dummyObserver2' }); dummySelectorWeakMap = new WeakMap(); dummyProxyWeakMap = new WeakMap(); + + jest.clearAllMocks(); }); it('should create CallbackSubscriptionContainer', () => { diff --git a/packages/core/tests/unit/runtime/subscription/container/ComponentSubscriptionContainer.test.ts b/packages/core/tests/unit/runtime/subscription/container/ComponentSubscriptionContainer.test.ts index 4401cf88..9efe4262 100644 --- a/packages/core/tests/unit/runtime/subscription/container/ComponentSubscriptionContainer.test.ts +++ b/packages/core/tests/unit/runtime/subscription/container/ComponentSubscriptionContainer.test.ts @@ -15,7 +15,6 @@ describe('ComponentSubscriptionContainer Tests', () => { let dummyProxyWeakMap: ProxyWeakMapType; beforeEach(() => { - jest.clearAllMocks(); LogMock.mockLogs(); dummyAgile = new Agile(); @@ -23,6 +22,8 @@ describe('ComponentSubscriptionContainer Tests', () => { dummyObserver2 = new Observer(dummyAgile, { key: 'dummyObserver2' }); dummySelectorWeakMap = new WeakMap(); dummyProxyWeakMap = new WeakMap(); + + jest.clearAllMocks(); }); it('should create ComponentSubscriptionContainer', () => { diff --git a/packages/core/tests/unit/runtime/subscription/container/SubscriptionContainer.test.ts b/packages/core/tests/unit/runtime/subscription/container/SubscriptionContainer.test.ts index c3aa012d..ce983e95 100644 --- a/packages/core/tests/unit/runtime/subscription/container/SubscriptionContainer.test.ts +++ b/packages/core/tests/unit/runtime/subscription/container/SubscriptionContainer.test.ts @@ -16,7 +16,6 @@ describe('SubscriptionContainer Tests', () => { let dummyProxyWeakMap: ProxyWeakMapType; beforeEach(() => { - jest.clearAllMocks(); LogMock.mockLogs(); dummyAgile = new Agile(); @@ -24,6 +23,8 @@ describe('SubscriptionContainer Tests', () => { dummyObserver2 = new Observer(dummyAgile, { key: 'dummyObserver2' }); dummySelectorWeakMap = new WeakMap(); dummyProxyWeakMap = new WeakMap(); + + jest.clearAllMocks(); }); it('should create SubscriptionContainer with passed subs array (default config)', () => { diff --git a/packages/core/tests/unit/runtime/subscription/sub.controller.test.ts b/packages/core/tests/unit/runtime/subscription/sub.controller.test.ts index e52e81a1..6d73d851 100644 --- a/packages/core/tests/unit/runtime/subscription/sub.controller.test.ts +++ b/packages/core/tests/unit/runtime/subscription/sub.controller.test.ts @@ -12,10 +12,11 @@ describe('SubController Tests', () => { let dummyAgile: Agile; beforeEach(() => { - jest.clearAllMocks(); LogMock.mockLogs(); dummyAgile = new Agile({ localStorage: false }); + + jest.clearAllMocks(); }); it('should create SubController', () => { diff --git a/packages/core/tests/unit/shared.test.ts b/packages/core/tests/unit/shared.test.ts new file mode 100644 index 00000000..fc81e6e8 --- /dev/null +++ b/packages/core/tests/unit/shared.test.ts @@ -0,0 +1,212 @@ +import { + Agile, + Collection, + Computed, + shared, + State, + Storage, + createStorage, + createState, + createCollection, + createComputed, + assignSharedAgileInstance, +} from '../../src'; +import { LogMock } from '../helper/logMock'; + +jest.mock('../../src/storages/storage'); +jest.mock('../../src/collection'); +jest.mock('../../src/computed'); + +// https://github.com/facebook/jest/issues/5023 +jest.mock('../../src/state', () => { + return { + State: jest.fn(), + }; +}); + +describe('Shared Tests', () => { + let sharedAgileInstance: Agile; + + beforeEach(() => { + LogMock.mockLogs(); + + sharedAgileInstance = new Agile(); + assignSharedAgileInstance(sharedAgileInstance); + + jest.clearAllMocks(); + }); + + describe('assignSharedAgileInstance function tests', () => { + it('should assign the specified Agile Instance as new shared Agile Instance', () => { + const newAgileInstance = new Agile({ key: 'notShared' }); + + assignSharedAgileInstance(newAgileInstance); + + expect(shared).toBe(newAgileInstance); + }); + }); + + describe('createStorage function tests', () => { + const StorageMock = Storage as jest.MockedClass; + + beforeEach(() => { + StorageMock.mockClear(); + }); + + it('should create Storage', () => { + const storageConfig = { + prefix: 'test', + methods: { + get: () => { + /* empty function */ + }, + set: () => { + /* empty function */ + }, + remove: () => { + /* empty function */ + }, + }, + key: 'myTestStorage', + }; + + const storage = createStorage(storageConfig); + + expect(storage).toBeInstanceOf(Storage); + expect(StorageMock).toHaveBeenCalledWith(storageConfig); + }); + }); + + describe('createState function tests', () => { + const StateMock = State as jest.MockedClass; + + it('should create State with the shared Agile Instance', () => { + const state = createState('testValue', { + key: 'myCoolState', + }); + + expect(state).toBeInstanceOf(State); + expect(StateMock).toHaveBeenCalledWith(sharedAgileInstance, 'testValue', { + key: 'myCoolState', + }); + }); + + it('should create State with a specified Agile Instance', () => { + const agile = new Agile(); + + const state = createState('testValue', { + key: 'myCoolState', + agileInstance: agile, + }); + + expect(state).toBeInstanceOf(State); + expect(StateMock).toHaveBeenCalledWith(agile, 'testValue', { + key: 'myCoolState', + }); + }); + }); + + describe('createCollection function tests', () => { + const CollectionMock = Collection as jest.MockedClass; + + beforeEach(() => { + CollectionMock.mockClear(); + }); + + it('should create Collection with the shared Agile Instance', () => { + const collectionConfig = { + selectors: ['test', 'test1'], + groups: ['test2', 'test10'], + defaultGroupKey: 'frank', + key: 'myCoolCollection', + }; + + const collection = createCollection(collectionConfig); + + expect(collection).toBeInstanceOf(Collection); + expect(CollectionMock).toHaveBeenCalledWith( + sharedAgileInstance, + collectionConfig + ); + }); + + it('should create Collection with a specified Agile Instance', () => { + const agile = new Agile(); + const collectionConfig = { + selectors: ['test', 'test1'], + groups: ['test2', 'test10'], + defaultGroupKey: 'frank', + key: 'myCoolCollection', + }; + + const collection = createCollection(collectionConfig, agile); + + expect(collection).toBeInstanceOf(Collection); + expect(CollectionMock).toHaveBeenCalledWith(agile, collectionConfig); + }); + }); + + describe('createComputed function tests', () => { + const ComputedMock = Computed as jest.MockedClass; + const computedFunction = () => { + // empty + }; + + beforeEach(() => { + ComputedMock.mockClear(); + }); + + it('should create Computed with the shared Agile Instance (default config)', () => { + const response = createComputed(computedFunction, ['dummyDep' as any]); + + expect(response).toBeInstanceOf(Computed); + expect(ComputedMock).toHaveBeenCalledWith( + sharedAgileInstance, + computedFunction, + { + computedDeps: ['dummyDep' as any], + } + ); + }); + + it('should create Computed with the shared Agile Instance (specific config)', () => { + const computedConfig = { + key: 'jeff', + isPlaceholder: false, + computedDeps: ['dummyDep' as any], + autodetect: true, + }; + + const response = createComputed(computedFunction, computedConfig); + + expect(response).toBeInstanceOf(Computed); + expect(ComputedMock).toHaveBeenCalledWith( + sharedAgileInstance, + computedFunction, + computedConfig + ); + }); + + it('should create Computed with a specified Agile Instance (specific config)', () => { + const agile = new Agile(); + const computedConfig = { + key: 'jeff', + isPlaceholder: false, + computedDeps: ['dummyDep' as any], + autodetect: true, + }; + + const response = createComputed(computedFunction, { + ...computedConfig, + ...{ agileInstance: agile }, + }); + + expect(response).toBeInstanceOf(Computed); + expect(ComputedMock).toHaveBeenCalledWith( + agile, + computedFunction, + computedConfig + ); + }); + }); +}); diff --git a/packages/core/tests/unit/state/state.observer.test.ts b/packages/core/tests/unit/state/state.observer.test.ts index 32a99109..035ec4a5 100644 --- a/packages/core/tests/unit/state/state.observer.test.ts +++ b/packages/core/tests/unit/state/state.observer.test.ts @@ -17,11 +17,12 @@ describe('StateObserver Tests', () => { let dummyState: State; beforeEach(() => { - jest.clearAllMocks(); LogMock.mockLogs(); dummyAgile = new Agile({ localStorage: false }); dummyState = new State(dummyAgile, 'dummyValue', { key: 'dummyState' }); + + jest.clearAllMocks(); }); it('should create State Observer (default config)', () => { diff --git a/packages/core/tests/unit/state/state.persistent.test.ts b/packages/core/tests/unit/state/state.persistent.test.ts index 1fc7d061..65603523 100644 --- a/packages/core/tests/unit/state/state.persistent.test.ts +++ b/packages/core/tests/unit/state/state.persistent.test.ts @@ -12,7 +12,6 @@ describe('StatePersistent Tests', () => { let dummyState: State; beforeEach(() => { - jest.clearAllMocks(); LogMock.mockLogs(); dummyAgile = new Agile({ localStorage: false }); @@ -20,6 +19,8 @@ describe('StatePersistent Tests', () => { jest.spyOn(StatePersistent.prototype, 'instantiatePersistent'); jest.spyOn(StatePersistent.prototype, 'initialLoading'); + + jest.clearAllMocks(); }); it("should create StatePersistent and shouldn't call initialLoading if Persistent isn't ready (default config)", () => { diff --git a/packages/core/tests/unit/state/state.runtime.job.test.ts b/packages/core/tests/unit/state/state.runtime.job.test.ts index 58906f56..85df6e56 100644 --- a/packages/core/tests/unit/state/state.runtime.job.test.ts +++ b/packages/core/tests/unit/state/state.runtime.job.test.ts @@ -16,7 +16,6 @@ describe('RuntimeJob Tests', () => { let dummyObserver: StateObserver; beforeEach(() => { - jest.clearAllMocks(); LogMock.mockLogs(); dummyAgile = new Agile({ localStorage: false }); @@ -25,6 +24,8 @@ describe('RuntimeJob Tests', () => { }); dummyState = new State(dummyAgile, 'dummyValue'); dummyObserver = new StateObserver(dummyState); + + jest.clearAllMocks(); }); it( diff --git a/packages/core/tests/unit/state/state.test.ts b/packages/core/tests/unit/state/state.test.ts index 3bfe253f..78b3afe9 100644 --- a/packages/core/tests/unit/state/state.test.ts +++ b/packages/core/tests/unit/state/state.test.ts @@ -15,12 +15,13 @@ describe('State Tests', () => { let dummyAgile: Agile; beforeEach(() => { - jest.clearAllMocks(); LogMock.mockLogs(); dummyAgile = new Agile({ localStorage: false }); jest.spyOn(State.prototype, 'set'); + + jest.clearAllMocks(); }); it('should create State and should call initial set (default config)', () => { diff --git a/packages/core/tests/unit/storages/persistent.test.ts b/packages/core/tests/unit/storages/persistent.test.ts index 79ca7ca2..b038fff3 100644 --- a/packages/core/tests/unit/storages/persistent.test.ts +++ b/packages/core/tests/unit/storages/persistent.test.ts @@ -5,12 +5,13 @@ describe('Persistent Tests', () => { let dummyAgile: Agile; beforeEach(() => { - jest.clearAllMocks(); LogMock.mockLogs(); dummyAgile = new Agile({ localStorage: false }); jest.spyOn(Persistent.prototype, 'instantiatePersistent'); + + jest.clearAllMocks(); }); it('should create Persistent (default config)', () => { diff --git a/packages/core/tests/unit/storages/storage.test.ts b/packages/core/tests/unit/storages/storage.test.ts index 1d1442c8..bfe220c0 100644 --- a/packages/core/tests/unit/storages/storage.test.ts +++ b/packages/core/tests/unit/storages/storage.test.ts @@ -5,7 +5,6 @@ describe('Storage Tests', () => { let dummyStorageMethods; beforeEach(() => { - jest.clearAllMocks(); LogMock.mockLogs(); dummyStorageMethods = { @@ -16,6 +15,8 @@ describe('Storage Tests', () => { // https://codewithhugo.com/jest-stub-mock-spy-set-clear/ jest.spyOn(Storage.prototype, 'validate'); + + jest.clearAllMocks(); }); it('should create not async Storage with normal Storage Methods (default config)', () => { diff --git a/packages/core/tests/unit/storages/storages.test.ts b/packages/core/tests/unit/storages/storages.test.ts index efdb57a3..139a7e29 100644 --- a/packages/core/tests/unit/storages/storages.test.ts +++ b/packages/core/tests/unit/storages/storages.test.ts @@ -5,12 +5,13 @@ describe('Storages Tests', () => { let dummyAgile; beforeEach(() => { - jest.clearAllMocks(); LogMock.mockLogs(); dummyAgile = new Agile({ localStorage: false }); jest.spyOn(Storages.prototype, 'instantiateLocalStorage'); + + jest.clearAllMocks(); }); it('should create Storages (default config)', () => { diff --git a/packages/core/tests/unit/utils.test.ts b/packages/core/tests/unit/utils.test.ts index db4bd7e3..96decb42 100644 --- a/packages/core/tests/unit/utils.test.ts +++ b/packages/core/tests/unit/utils.test.ts @@ -1,12 +1,11 @@ import { - globalBind, - getAgileInstance, Agile, State, Observer, Collection, StateObserver, GroupObserver, + assignSharedAgileInstance, } from '../../src'; import * as Utils from '../../src/utils'; import { LogMock } from '../helper/logMock'; @@ -15,47 +14,68 @@ describe('Utils Tests', () => { let dummyAgile: Agile; beforeEach(() => { - jest.clearAllMocks(); LogMock.mockLogs(); dummyAgile = new Agile({ localStorage: false }); // @ts-ignore | Reset globalThis globalThis = {}; + + jest.clearAllMocks(); }); describe('getAgileInstance function tests', () => { beforeEach(() => { + assignSharedAgileInstance(dummyAgile); globalThis[Agile.globalKey] = dummyAgile; }); - it('should get agileInstance from State', () => { + it('should return Agile Instance from State', () => { const dummyState = new State(dummyAgile, 'dummyValue'); - expect(getAgileInstance(dummyState)).toBe(dummyAgile); + expect(Utils.getAgileInstance(dummyState)).toBe(dummyAgile); }); - it('should get agileInstance from Collection', () => { + it('should return Agile Instance from Collection', () => { const dummyCollection = new Collection(dummyAgile); - expect(getAgileInstance(dummyCollection)).toBe(dummyAgile); + expect(Utils.getAgileInstance(dummyCollection)).toBe(dummyAgile); }); - it('should get agileInstance from Observer', () => { + it('should return Agile Instance from Observer', () => { const dummyObserver = new Observer(dummyAgile); - expect(getAgileInstance(dummyObserver)).toBe(dummyAgile); + expect(Utils.getAgileInstance(dummyObserver)).toBe(dummyAgile); }); - it('should get agileInstance from globalThis if passed instance holds no agileInstance', () => { - expect(getAgileInstance('weiredInstance')).toBe(dummyAgile); - }); - - it('should print error if something went wrong', () => { + it( + 'should return shared Agile Instance ' + + 'if specified Instance contains no valid Agile Instance', + () => { + expect(Utils.getAgileInstance('weiredInstance')).toBe(dummyAgile); + } + ); + + it( + 'should return globally bound Agile Instance' + + 'if specified Instance contains no valid Agile Instance' + + 'and no shared Agile Instance is specified', + () => { + // Destroy shared Agile Instance + assignSharedAgileInstance(undefined as any); + + expect(Utils.getAgileInstance('weiredInstance')).toBe(dummyAgile); + } + ); + + it('should print error if no Agile Instance could be retrieved', () => { // @ts-ignore | Destroy globalThis globalThis = undefined; - const response = getAgileInstance('weiredInstance'); + // Destroy shared Agile Instance + assignSharedAgileInstance(undefined as any); + + const response = Utils.getAgileInstance('weiredInstance'); expect(response).toBeUndefined(); LogMock.hasLoggedCode('20:03:00', [], 'weiredInstance'); @@ -360,23 +380,23 @@ describe('Utils Tests', () => { }); it('should bind Instance globally at the specified key (default config)', () => { - globalBind(dummyKey, 'dummyInstance'); + Utils.globalBind(dummyKey, 'dummyInstance'); expect(globalThis[dummyKey]).toBe('dummyInstance'); }); it("shouldn't overwrite already globally bound Instance at the same key (default config)", () => { - globalBind(dummyKey, 'I am first!'); + Utils.globalBind(dummyKey, 'I am first!'); - globalBind(dummyKey, 'dummyInstance'); + Utils.globalBind(dummyKey, 'dummyInstance'); expect(globalThis[dummyKey]).toBe('I am first!'); }); it('should overwrite already globally bound Instance at the same key (overwrite = true)', () => { - globalBind(dummyKey, 'I am first!'); + Utils.globalBind(dummyKey, 'I am first!'); - globalBind(dummyKey, 'dummyInstance', true); + Utils.globalBind(dummyKey, 'dummyInstance', true); expect(globalThis[dummyKey]).toBe('dummyInstance'); }); @@ -385,9 +405,27 @@ describe('Utils Tests', () => { // @ts-ignore | Destroy globalThis globalThis = undefined; - globalBind(dummyKey, 'dummyInstance'); + Utils.globalBind(dummyKey, 'dummyInstance'); LogMock.hasLoggedCode('20:03:01', [dummyKey]); }); }); + + describe('runsOnServer function tests', () => { + it("should return 'false' if the current environment isn't a server", () => { + global.window = { + document: { + createElement: 'isSet' as any, + } as any, + } as any; + + expect(Utils.runsOnServer()).toBeFalsy(); + }); + + it("should return 'true' if the current environment is a server", () => { + global.window = undefined as any; + + expect(Utils.runsOnServer()).toBeTruthy(); + }); + }); }); diff --git a/packages/event/tests/unit/event.job.test.ts b/packages/event/tests/unit/event.job.test.ts index 7c475015..90397943 100644 --- a/packages/event/tests/unit/event.job.test.ts +++ b/packages/event/tests/unit/event.job.test.ts @@ -1,10 +1,10 @@ import { EventJob } from '../../src'; -import mockConsole from 'jest-mock-console'; +import { LogMock } from '../../../core/tests/helper/logMock'; describe('EventJob Tests', () => { beforeEach(() => { + LogMock.mockLogs(); jest.clearAllMocks(); - mockConsole(['error', 'warn']); }); it('should create EventJob (without keys)', () => { diff --git a/packages/event/tests/unit/event.observer.test.ts b/packages/event/tests/unit/event.observer.test.ts index 221bd2b9..b700a72f 100644 --- a/packages/event/tests/unit/event.observer.test.ts +++ b/packages/event/tests/unit/event.observer.test.ts @@ -1,17 +1,18 @@ import { EventObserver, Event } from '../../src'; import { Agile, Observer, SubscriptionContainer } from '@agile-ts/core'; -import mockConsole from 'jest-mock-console'; +import { LogMock } from '../../../core/tests/helper/logMock'; describe('EventObserver Tests', () => { let dummyAgile: Agile; let dummyEvent: Event; beforeEach(() => { - jest.clearAllMocks(); - mockConsole(['error', 'warn']); + LogMock.mockLogs(); dummyAgile = new Agile({ localStorage: false }); dummyEvent = new Event(dummyAgile); + + jest.clearAllMocks(); }); it('should create EventObserver (default config)', () => { diff --git a/packages/event/tests/unit/event.test.ts b/packages/event/tests/unit/event.test.ts index f2bdd61f..bd369952 100644 --- a/packages/event/tests/unit/event.test.ts +++ b/packages/event/tests/unit/event.test.ts @@ -1,16 +1,17 @@ import { Event, EventObserver } from '../../src'; import { Agile, Observer } from '@agile-ts/core'; import * as Utils from '@agile-ts/utils'; -import mockConsole from 'jest-mock-console'; +import { LogMock } from '../../../core/tests/helper/logMock'; describe('Event Tests', () => { let dummyAgile: Agile; beforeEach(() => { - jest.clearAllMocks(); - mockConsole(['error', 'warn']); + LogMock.mockLogs(); dummyAgile = new Agile({ localStorage: false }); + + jest.clearAllMocks(); }); it('should create Event (default config)', () => { diff --git a/packages/react/src/hooks/useIsomorphicLayoutEffect.ts b/packages/react/src/hooks/useIsomorphicLayoutEffect.ts index 9e397c06..becea846 100644 --- a/packages/react/src/hooks/useIsomorphicLayoutEffect.ts +++ b/packages/react/src/hooks/useIsomorphicLayoutEffect.ts @@ -1,4 +1,5 @@ import { useEffect, useLayoutEffect } from 'react'; +import { runsOnServer } from '@agile-ts/core'; // React currently throws a warning when using useLayoutEffect on the server. // To get around it, we can conditionally useEffect on the server (no-op) and @@ -9,9 +10,6 @@ import { useEffect, useLayoutEffect } from 'react'; // is created synchronously, otherwise a store update may occur before the // subscription is created and an inconsistent state may be observed -export const useIsomorphicLayoutEffect = - typeof window !== 'undefined' && - typeof window.document !== 'undefined' && - typeof window.document.createElement !== 'undefined' - ? useLayoutEffect - : useEffect; +export const useIsomorphicLayoutEffect = runsOnServer() + ? useLayoutEffect + : useEffect; diff --git a/packages/react/src/react.integration.ts b/packages/react/src/react.integration.ts index a0bf8774..2ba63ec8 100644 --- a/packages/react/src/react.integration.ts +++ b/packages/react/src/react.integration.ts @@ -1,4 +1,4 @@ -import { Agile, flatMerge, Integration } from '@agile-ts/core'; +import { flatMerge, Integration, Integrations } from '@agile-ts/core'; import { AgileReactComponent } from './hocs/AgileHOC'; import React from 'react'; @@ -24,6 +24,6 @@ const reactIntegration = new Integration({ } }, }); -Agile.initialIntegrations.push(reactIntegration); +Integrations.addInitialIntegration(reactIntegration); export default reactIntegration; diff --git a/packages/vue/src/vue.integration.ts b/packages/vue/src/vue.integration.ts index cf2268b9..d6a367ec 100644 --- a/packages/vue/src/vue.integration.ts +++ b/packages/vue/src/vue.integration.ts @@ -1,4 +1,4 @@ -import Agile, { Integration } from '@agile-ts/core'; +import Agile, { Integration, Integrations } from '@agile-ts/core'; import Vue from 'vue'; import { bindAgileInstances, DepsType } from './bindAgileInstances'; @@ -80,6 +80,6 @@ const vueIntegration = new Integration({ return Promise.resolve(true); }, }); -Agile.initialIntegrations.push(vueIntegration); +Integrations.addInitialIntegration(vueIntegration); export default vueIntegration;