From d27ee983fbe97535738577e743eb50ac55c538e0 Mon Sep 17 00:00:00 2001 From: * <*> Date: Fri, 15 Jul 2022 21:14:22 +0800 Subject: [PATCH 01/38] Match-id-ee12b2c587d5ebf5b2b49cd40df333ed3386118d --- jest.config.js | 3 +- libs/horizon/src/horizonx/adapters/redux.ts | 35 ++- .../src/horizonx/adapters/reduxReact.ts | 37 +-- .../src/horizonx/adapters/reduxThunk.ts | 3 +- .../src/horizonx/proxy/HooklessObserver.ts | 2 +- libs/horizon/src/horizonx/proxy/Observer.ts | 21 +- .../src/horizonx/proxy/ProxyHandler.ts | 2 +- .../src/horizonx/store/StoreHandler.ts | 163 ++++++---- libs/horizon/src/horizonx/types.d.ts | 81 ----- package.json | 6 +- .../StateManager/StateArray.test.js | 201 ------------- .../StateManager/StateArray.test.tsx | 208 +++++++++++++ .../{StateMap.test.js => StateMap.test.tsx} | 152 +++++----- ...eMixType.test.js => StateMixType.test.tsx} | 37 ++- .../{StateSet.test.js => StateSet.test.tsx} | 151 +++++----- ...eWeakMap.test.js => StateWeakMap.test.tsx} | 80 ++--- ...eWeakSet.test.js => StateWeakSet.test.tsx} | 73 +++-- .../{async.test.js => async.test.tsx} | 27 +- .../StoreFunctionality/basicAccess.test.js | 63 ---- .../StoreFunctionality/basicAccess.test.tsx | 110 +++++++ ...arAccess.test.js => dollarAccess.test.tsx} | 20 +- ...otherCases.test.js => otherCases.test.tsx} | 36 +-- .../{reset.test.js => reset.js} | 42 ++- .../StoreFunctionality/{store.js => store.ts} | 0 ...xAdapter.test.js => ReduxAdapter.test.tsx} | 153 +++++----- .../adapters/ReduxAdapterThunk.test.js | 34 --- .../adapters/ReduxAdapterThunk.test.tsx | 33 +++ ...ter.test.js => ReduxReactAdapter.test.tsx} | 278 +++++++----------- ...eption.test.js => ClassException.test.tsx} | 38 +-- .../class/ClassStateArray.test.js | 220 -------------- .../class/ClassStateArray.test.tsx | 231 +++++++++++++++ ...tateMap.test.js => ClassStateMap.test.tsx} | 165 ++++++----- ...Clear.test.js => ClassVNodeClear.test.tsx} | 63 ++-- ...ar.test.js => FunctionVNodeClear.test.tsx} | 65 ++-- .../HorizonXText/edgeCases/proxy.test.js | 21 -- .../HorizonXText/edgeCases/proxy.test.tsx | 48 +++ scripts/__tests__/jest/logUtils.js | 26 ++ scripts/rollup/rollup.config.js | 2 + 38 files changed, 1538 insertions(+), 1392 deletions(-) delete mode 100644 libs/horizon/src/horizonx/types.d.ts delete mode 100644 scripts/__tests__/HorizonXText/StateManager/StateArray.test.js create mode 100644 scripts/__tests__/HorizonXText/StateManager/StateArray.test.tsx rename scripts/__tests__/HorizonXText/StateManager/{StateMap.test.js => StateMap.test.tsx} (60%) rename scripts/__tests__/HorizonXText/StateManager/{StateMixType.test.js => StateMixType.test.tsx} (77%) rename scripts/__tests__/HorizonXText/StateManager/{StateSet.test.js => StateSet.test.tsx} (58%) rename scripts/__tests__/HorizonXText/StateManager/{StateWeakMap.test.js => StateWeakMap.test.tsx} (61%) rename scripts/__tests__/HorizonXText/StateManager/{StateWeakSet.test.js => StateWeakSet.test.tsx} (59%) rename scripts/__tests__/HorizonXText/StoreFunctionality/{async.test.js => async.test.tsx} (80%) delete mode 100644 scripts/__tests__/HorizonXText/StoreFunctionality/basicAccess.test.js create mode 100644 scripts/__tests__/HorizonXText/StoreFunctionality/basicAccess.test.tsx rename scripts/__tests__/HorizonXText/StoreFunctionality/{dollarAccess.test.js => dollarAccess.test.tsx} (64%) rename scripts/__tests__/HorizonXText/StoreFunctionality/{otherCases.test.js => otherCases.test.tsx} (71%) rename scripts/__tests__/HorizonXText/StoreFunctionality/{reset.test.js => reset.js} (65%) rename scripts/__tests__/HorizonXText/StoreFunctionality/{store.js => store.ts} (100%) rename scripts/__tests__/HorizonXText/adapters/{ReduxAdapter.test.js => ReduxAdapter.test.tsx} (59%) delete mode 100644 scripts/__tests__/HorizonXText/adapters/ReduxAdapterThunk.test.js create mode 100644 scripts/__tests__/HorizonXText/adapters/ReduxAdapterThunk.test.tsx rename scripts/__tests__/HorizonXText/adapters/{ReduxReactAdapter.test.js => ReduxReactAdapter.test.tsx} (50%) rename scripts/__tests__/HorizonXText/class/{ClassException.test.js => ClassException.test.tsx} (54%) delete mode 100644 scripts/__tests__/HorizonXText/class/ClassStateArray.test.js create mode 100644 scripts/__tests__/HorizonXText/class/ClassStateArray.test.tsx rename scripts/__tests__/HorizonXText/class/{ClassStateMap.test.js => ClassStateMap.test.tsx} (58%) rename scripts/__tests__/HorizonXText/clear/{ClassVNodeClear.test.js => ClassVNodeClear.test.tsx} (62%) rename scripts/__tests__/HorizonXText/clear/{FunctionVNodeClear.test.js => FunctionVNodeClear.test.tsx} (59%) delete mode 100644 scripts/__tests__/HorizonXText/edgeCases/proxy.test.js create mode 100644 scripts/__tests__/HorizonXText/edgeCases/proxy.test.tsx create mode 100644 scripts/__tests__/jest/logUtils.js diff --git a/jest.config.js b/jest.config.js index 665708c9..dde828dd 100644 --- a/jest.config.js +++ b/jest.config.js @@ -138,7 +138,8 @@ module.exports = { // The glob patterns Jest uses to detect test files testMatch: [ - '/scripts/__tests__/**/*.test.js' + '/scripts/__tests__/**/*.test.js', + '/scripts/__tests__/**/*.test.tsx' ], // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped diff --git a/libs/horizon/src/horizonx/adapters/redux.ts b/libs/horizon/src/horizonx/adapters/redux.ts index 293700b6..fb19cf9a 100644 --- a/libs/horizon/src/horizonx/adapters/redux.ts +++ b/libs/horizon/src/horizonx/adapters/redux.ts @@ -1,14 +1,38 @@ import { createStore as createStoreX } from '../store/StoreHandler'; -import { ReduxStoreHandler, ReduxAction, ReduxMiddleware } from '../types'; +import { ReduxStoreHandler } from '../store/StoreHandler'; export { thunk } from './reduxThunk'; -export { Provider, useSelector, useStore, useDispatch, connect, createSelectorHook, createDispatchHook } from './reduxReact'; +export { + Provider, + useSelector, + useStore, + useDispatch, + connect, + createSelectorHook, + createDispatchHook, +} from './reduxReact'; + +export type ReduxAction = { + type: string; + [key: string]: any; +}; + +export type ReduxMiddleware = ( + store: ReduxStoreHandler, + extraArgument?: any +) => ( + next: (action: ReduxAction) => any +) => ( + action: + | ReduxAction + | ((dispatch: (action: ReduxAction) => void, store: ReduxStoreHandler, extraArgument?: any) => any) +) => ReduxStoreHandler; type Reducer = (state: any, action: ReduxAction) => any; -export function createStore(reducer: Reducer, preloadedState: any, enhancers): ReduxStoreHandler { +export function createStore(reducer: Reducer, preloadedState?: any, enhancers?): ReduxStoreHandler { const store = createStoreX({ id: 'defaultStore', state: { stateWrapper: preloadedState }, @@ -35,7 +59,7 @@ export function createStore(reducer: Reducer, preloadedState: any, enhancers): R const result = { reducer, getState: function() { - return store.$state.stateWrapper; + return store.$s.stateWrapper; }, subscribe: listener => { store.$subscribe(listener); @@ -48,7 +72,7 @@ export function createStore(reducer: Reducer, preloadedState: any, enhancers): R reducer = newReducer; }, _horizonXstore: store, - dispatch: store.$actions.dispatch, + dispatch: store.$a.dispatch, }; enhancers && enhancers(result); @@ -117,7 +141,6 @@ export function compose(middlewares: ReduxMiddleware[]) { }; } - // HorizonX batches updates by default, this function is only for backwards compatibility export function batch(fn: () => void) { fn(); diff --git a/libs/horizon/src/horizonx/adapters/reduxReact.ts b/libs/horizon/src/horizonx/adapters/reduxReact.ts index da99ab52..8e205eb7 100644 --- a/libs/horizon/src/horizonx/adapters/reduxReact.ts +++ b/libs/horizon/src/horizonx/adapters/reduxReact.ts @@ -3,9 +3,10 @@ import { useState, useContext, useEffect, useRef } from '../../renderer/hooks/Ho import { createContext } from '../../renderer/components/context/CreateContext'; import { createElement } from '../../external/JSXElement'; import { BoundActionCreator } from './redux'; -import { ReduxAction, ReduxStoreHandler } from '../types'; +import { ReduxAction } from './redux'; +import { ReduxStoreHandler } from '../store/StoreHandler' -const DefaultContext = createContext(); +const DefaultContext = createContext(null); type Context = typeof DefaultContext; export function Provider({ @@ -27,8 +28,8 @@ export function createStoreHook(context: Context) { }; } -export function createSelectorHook(context: Context): (selector: (any) => any) => any { - const store = createStoreHook(context)(); +export function createSelectorHook(context: Context): (selector?: (any) => any) => any { + const store = (createStoreHook(context)() as unknown) as ReduxStoreHandler; return function(selector = state => state) { const [b, fr] = useState(false); @@ -37,22 +38,18 @@ export function createSelectorHook(context: Context): (selector: (any) => any) = }; useEffect(() => { - const unsubscribe = store.subscribe(listener); - - return () => { - unsubscribe(listener); - }; + return store.subscribe(listener); }); return selector(store.getState()); }; } -export function createDispatchHook(context: Context): BoundActionCreator { - const store = createStoreHook(context)(); +export function createDispatchHook(context: Context): ()=>BoundActionCreator { + const store = (createStoreHook(context)() as unknown) as ReduxStoreHandler; return function() { return action => { - this.dispatch(action); + store.dispatch(action); }; }.bind(store); } @@ -104,26 +101,32 @@ export function connect( } return Component => { - const useStore = createStoreHook(options.context || DefaultContext); + const useStore = createStoreHook(options?.context || DefaultContext); function Wrapper(props) { const [f, forceReload] = useState(true); - const store = useStore(); + const store = (useStore() as unknown) as ReduxStoreHandler; useEffect(() => { const unsubscribe = store.subscribe(() => forceReload(!f)); () => { - unsubscribe(() => forceReload(!f)); + unsubscribe(); }; }); const previous = useRef({ state: {}, - }); + mappedState: {}, + }) as { + current: { + state: {}; + mappedState: {}; + }; + }; let mappedState; - if (options.areStatesEqual) { + if (options?.areStatesEqual) { if (options.areStatesEqual(previous.current.state, store.getState())) { mappedState = previous.current.mappedState; } else { diff --git a/libs/horizon/src/horizonx/adapters/reduxThunk.ts b/libs/horizon/src/horizonx/adapters/reduxThunk.ts index 850f1b1c..28ce23e0 100644 --- a/libs/horizon/src/horizonx/adapters/reduxThunk.ts +++ b/libs/horizon/src/horizonx/adapters/reduxThunk.ts @@ -1,4 +1,5 @@ -import { ReduxStoreHandler, ReduxAction, ReduxMiddleware } from '../types'; +import { ReduxAction, ReduxMiddleware } from './redux'; +import { ReduxStoreHandler } from '../store/StoreHandler'; function createThunkMiddleware(extraArgument?: any): ReduxMiddleware { return (store: ReduxStoreHandler) => (next: (action: ReduxAction) => any) => ( diff --git a/libs/horizon/src/horizonx/proxy/HooklessObserver.ts b/libs/horizon/src/horizonx/proxy/HooklessObserver.ts index 9ea8937f..1de98c5f 100644 --- a/libs/horizon/src/horizonx/proxy/HooklessObserver.ts +++ b/libs/horizon/src/horizonx/proxy/HooklessObserver.ts @@ -1,6 +1,6 @@ // TODO: implement vNode type -import {IObserver} from '../types'; +import {IObserver} from './Observer'; /** * 一个对象(对象、数组、集合)对应一个Observer diff --git a/libs/horizon/src/horizonx/proxy/Observer.ts b/libs/horizon/src/horizonx/proxy/Observer.ts index be24d1c2..a6348841 100644 --- a/libs/horizon/src/horizonx/proxy/Observer.ts +++ b/libs/horizon/src/horizonx/proxy/Observer.ts @@ -6,7 +6,24 @@ import { launchUpdateFromVNode } from '../../renderer/TreeBuilder'; import { getProcessingVNode } from '../../renderer/GlobalVar'; import { VNode } from '../../renderer/vnode/VNode'; -import { IObserver } from '../types'; +export interface IObserver { + + useProp: (key: string) => void; + + addListener: (listener: () => void) => void; + + removeListener: (listener: () => void) => void; + + setProp: (key: string) => void; + + triggerChangeListeners: () => void; + + triggerUpdate: (vNode: any) => void; + + allChange: () => void; + + clearByVNode: (vNode: any) => void; +} export class Observer implements IObserver { @@ -85,7 +102,7 @@ export class Observer implements IObserver { } } - clearByVNode(vNode: Vnode): void { + clearByVNode(vNode: VNode): void { const keys = this.vNodeKeys.get(vNode); if (keys) { keys.forEach((key: any) => { diff --git a/libs/horizon/src/horizonx/proxy/ProxyHandler.ts b/libs/horizon/src/horizonx/proxy/ProxyHandler.ts index f2051b9d..b3519729 100644 --- a/libs/horizon/src/horizonx/proxy/ProxyHandler.ts +++ b/libs/horizon/src/horizonx/proxy/ProxyHandler.ts @@ -4,7 +4,7 @@ import {HooklessObserver} from './HooklessObserver'; import {isArray, isCollection, isObject} from '../CommonUtils'; import {createArrayProxy} from './handlers/ArrayProxyHandler'; import {createCollectionProxy} from './handlers/CollectionProxyHandler'; -import { IObserver } from '../types'; +import { IObserver } from './Observer'; const OBSERVER_KEY = Symbol('_horizonObserver'); diff --git a/libs/horizon/src/horizonx/store/StoreHandler.ts b/libs/horizon/src/horizonx/store/StoreHandler.ts index 9b746c96..ffc67aa8 100644 --- a/libs/horizon/src/horizonx/store/StoreHandler.ts +++ b/libs/horizon/src/horizonx/store/StoreHandler.ts @@ -3,31 +3,88 @@ import { useEffect, useRef } from '../../renderer/hooks/HookExternal'; import { getProcessingVNode } from '../../renderer/GlobalVar'; import { createProxy } from '../proxy/ProxyHandler'; import readonlyProxy from '../proxy/readonlyProxy'; -import { StoreHandler, StoreConfig, UserActions, UserComputedValues, StoreActions, ComputedValues, ActionFunction, Action, QueuedStoreActions } from '../types'; import { Observer } from '../proxy/Observer'; import { FunctionComponent, ClassComponent } from '../Constants'; +import { VNode } from '../../renderer/Types'; -const storeMap = new Map>(); +const storeMap = new Map>(); function isPromise(obj: any): boolean { return !!obj && (typeof obj === 'object' || typeof obj === 'function') && typeof obj.then === 'function'; } -type PlannedAction>={ - action:string, - payload: any[], - resolve: ReturnType -} +type StoreConfig, C extends UserComputedValues> = { + state?: S; + options?: { suppressHooks?: boolean }; + actions?: A; + id?: string; + computed?: C; +}; -export function createStore,C extends UserComputedValues>(config: StoreConfig): () => StoreHandler { +export type ReduxStoreHandler = { + reducer: (state: any, action: { type: string }) => any; + dispatch: (action: { type: string }) => void; + getState: () => any; + subscribe: (listener: () => void) => () => void; + replaceReducer: (reducer: (state: any, action: { type: string }) => any) => void; + _horizonXstore: StoreHandler; +}; + +type StoreHandler, C extends UserComputedValues> = { + $subscribe: (listener: () => void) => void; + $unsubscribe: (listener: () => void) => void; + $s: S; + $config: StoreConfig; + $queue: QueuedStoreActions; + $a: StoreActions; + $c: UserComputedValues; + reduxHandler?: ReduxStoreHandler; +} & { [K in keyof S]: S[K] } & + { [K in keyof A]: Action } & + { [K in keyof C]: ReturnType }; + +type PlannedAction> = { + action: string; + payload: any[]; + resolve: ReturnType; +}; +type RemoveFirstFromTuple = T['length'] extends 0 + ? [] + : ((...b: T) => void) extends (a, ...b: infer I) => void + ? I + : []; + +type UserActions = { [K: string]: ActionFunction }; +type UserComputedValues = { [K: string]: ComputedFunction }; + +type ActionFunction = (this: StoreHandler, state: S, ...args: any[]) => any; +type ComputedFunction = (state: S) => any; +type Action, S extends object> = ( + this: StoreHandler, + ...args: RemoveFirstFromTuple> +) => ReturnType; +type AsyncAction, S extends object> = ( + this: StoreHandler, + ...args: RemoveFirstFromTuple> +) => Promise>; + +type StoreActions> = { [K in keyof A]: Action }; +type QueuedStoreActions> = { [K in keyof A]: AsyncAction }; +type ComputedValues> = { [K in keyof C]: ReturnType }; +type PostponedAction = (state: object, ...args: any[]) => Promise; +type PostponedActions = { [key: string]: PostponedAction }; + +export function createStore, C extends UserComputedValues>( + config: StoreConfig +): () => StoreHandler { //create a local shalow copy to ensure consistency (if user would change the config object after store creation) config = { - id:config.id, + id: config.id, options: config.options, state: config.state, - actions: config.actions ? {...config.actions}:undefined, - computed: config.computed ? {...config.computed}:undefined - } + actions: config.actions ? { ...config.actions } : undefined, + computed: config.computed ? { ...config.computed } : undefined, + }; // 校验 if (Object.prototype.toString.call(config) !== '[object Object]') { @@ -35,30 +92,30 @@ export function createStore,C extends } const proxyObj = createProxy(config.state, !config.options?.suppressHooks); - + proxyObj.$pending = false; - - const $subscribe = (listener) => { + + const $subscribe = listener => { proxyObj.addListener(listener); }; - - const $unsubscribe = (listener) => { + + const $unsubscribe = listener => { proxyObj.removeListener(listener); }; - const plannedActions:PlannedAction>[] = []; - const $actions:Partial>={} - const $queue:Partial> = {}; - const $computed:Partial>={} - const handler = { + const plannedActions: PlannedAction>[] = []; + const $a: Partial> = {}; + const $queue: Partial> = {}; + const $c: Partial> = {}; + const handler = ({ $subscribe, $unsubscribe, - $actions:$actions as StoreActions, - $state:proxyObj, - $computed: $computed as ComputedValues, - $config:config, - $queue: $queue as QueuedStoreActions, - } as StoreHandler; + $a: $a as StoreActions, + $s: proxyObj, + $c: $c as ComputedValues, + $config: config, + $queue: $queue as QueuedStoreActions, + } as unknown) as StoreHandler; function tryNextAction() { if (!plannedActions.length) { @@ -67,7 +124,9 @@ export function createStore,C extends } const nextAction = plannedActions.shift()!; - const result = config.actions ? config.actions[nextAction.action].bind(self, proxyObj)(...nextAction.payload) : undefined; + const result = config.actions + ? config.actions[nextAction.action].bind(handler, proxyObj)(...nextAction.payload) + : undefined; if (isPromise(result)) { result.then(value => { @@ -81,16 +140,16 @@ export function createStore,C extends } // 包装actions - if(config.actions){ + if (config.actions) { Object.keys(config.actions).forEach(action => { ($queue as any)[action] = (...payload) => { - return new Promise((resolve) => { + return new Promise(resolve => { if (!proxyObj.$pending) { proxyObj.$pending = true; - const result = config.actions![action].bind(self, proxyObj)(...payload); - + const result = config.actions![action].bind(handler, proxyObj)(...payload); + if (isPromise(result)) { - result.then((value) => { + result.then(value => { resolve(value); tryNextAction(); }); @@ -102,40 +161,42 @@ export function createStore,C extends plannedActions.push({ action, payload, - resolve + resolve, }); } }); }; - - ($actions as any)[action] = function Wrapped(...payload) { - return config.actions![action].bind(self, proxyObj)(...payload); + + ($a as any)[action] = function Wrapped(...payload) { + return config.actions![action].bind(handler, proxyObj)(...payload); }; - // direct store access + // direct store access Object.defineProperty(handler, action, { writable: false, - value: $actions[action] + value: (...payload) => { + return config.actions![action].bind(handler, proxyObj)(...payload); + }, }); }); } - if (config.computed) { - Object.keys(config.computed).forEach((key) => { - ($computed as any)[key] = config.computed![key].bind(handler, readonlyProxy(proxyObj)); + if (config.computed) { + Object.keys(config.computed).forEach(key => { + ($c as any)[key] = config.computed![key].bind(handler, readonlyProxy(proxyObj)); - // direct store access + // direct store access Object.defineProperty(handler, key, { - get: $computed[key] as ()=>any + get: $c[key] as () => any, }); }); } // direct state access - if(config.state){ + if (config.state) { Object.keys(config.state).forEach(key => { Object.defineProperty(handler, key, { - get: () => proxyObj[key] + get: () => proxyObj[key], }); }); } @@ -172,7 +233,7 @@ function hookStore() { if (processingVNode.tag === FunctionComponent) { // from FunctionComponent - const vNodeRef = useRef(null); + const vNodeRef = (useRef(null) as unknown) as { current: VNode }; vNodeRef.current = processingVNode; useEffect(() => { @@ -211,9 +272,9 @@ export function useStore, C extends U if (storeObj && !storeObj.$config.options?.suppressHooks) hookStore(); - return storeObj as StoreHandler; + return storeObj as StoreHandler; } -export function clearStore(id:string):void { +export function clearStore(id: string): void { storeMap.delete(id); -} \ No newline at end of file +} diff --git a/libs/horizon/src/horizonx/types.d.ts b/libs/horizon/src/horizonx/types.d.ts deleted file mode 100644 index 3fed3afe..00000000 --- a/libs/horizon/src/horizonx/types.d.ts +++ /dev/null @@ -1,81 +0,0 @@ -export interface IObserver { - - useProp: (key: string) => void; - - addListener: (listener: () => void) => void; - - removeListener: (listener: () => void) => void; - - setProp: (key: string) => void; - - triggerChangeListeners: () => void; - - triggerUpdate: (vNode: any) => void; - - allChange: () => void; - - clearByVNode: (vNode: any) => void; -} - -type RemoveFirstFromTuple = - T['length'] extends 0 ? [] : - (((...b: T) => void) extends (a, ...b: infer I) => void ? I : []) - - -type UserActions = { [K:string]: ActionFunction }; -type UserComputedValues = { [K:string]: ComputedFunction }; - -type ActionFunction = (state: S, ...args: any[]) => any; -type ComputedFunction = (state: S) => any; -type Action> = (...args:RemoveFirstFromTuple>)=>ReturnType -type AsyncAction> = (...args:RemoveFirstFromTuple>)=>Promise> - -type StoreActions> = { [K in keyof A]: Action }; -type QueuedStoreActions> = { [K in keyof A]: AsyncAction }; -type ComputedValues> = { [K in keyof C]: ReturnType }; -type PostponedAction = (state: object, ...args: any[]) => Promise; -type PostponedActions = { [key:string]: PostponedAction } - -export type StoreHandler,C extends UserComputedValues> = - {$subscribe: ((listener: () => void) => void), - $unsubscribe: ((listener: () => void) => void), - $state: S, - $config: StoreConfig, - $queue: QueuedStoreActions, - $actions: StoreActions, - $computed: ComputedValues, - reduxHandler?:ReduxStoreHandler} - & - {[K in keyof S]: S[K]} - & - {[K in keyof A]: Action} - & - {[K in keyof C]: ReturnType} - -export type StoreConfig,C extends UserComputedValues> = { - state?: S, - options?:{suppressHooks?: boolean}, - actions?: A, - id?: string, - computed?: C -} - -type ReduxStoreHandler = { - reducer:(state:any,action:{type:string})=>any, - dispatch:(action:{type:string})=>void, - getState:()=>any, - subscribe:(listener:()=>void)=>((listener:()=>void)=>void) - replaceReducer: (reducer: (state:any,action:{type:string})=>any)=>void - _horizonXstore: StoreHandler -} - -type ReduxAction = { - type:string -} - -type ReduxMiddleware = (store:ReduxStoreHandler, extraArgument?:any) => - (next:((action:ReduxAction)=>any)) => - (action:( - ReduxAction| - ((dispatch:(action:ReduxAction)=>void,store:ReduxStoreHandler,extraArgument?:any)=>any) - )) => ReduxStoreHandler \ No newline at end of file diff --git a/package.json b/package.json index c31b27cf..7f37e8cd 100644 --- a/package.json +++ b/package.json @@ -5,11 +5,12 @@ ], "scripts": { "lint": "eslint . --ext .ts", - "build": " rollup --config ./scripts/rollup/rollup.config.js", - "build:watch": " rollup --watch --config ./scripts/rollup/rollup.config.js", + "build": "rollup --config ./scripts/rollup/rollup.config.js", + "build:watch": "rollup --watch --config ./scripts/rollup/rollup.config.js", "build-3rdLib": "node ./scripts/gen3rdLib.js", "build-3rdLib-dev": "npm run build & node ./scripts/gen3rdLib.js --dev", "build-horizon3rdLib-dev": "npm run build & node ./scripts/gen3rdLib.js --dev --type horizon", + "build-types": "tsc -p ./libs/horizon/index.ts --emitDeclarationOnly --declaration --declarationDir ./build/horizon/@types --skipLibCheck", "debug-test": "yarn test --debug", "test": "jest --config=jest.config.js", "watch-test": "yarn test --watch --dev" @@ -92,6 +93,7 @@ "regenerator-runtime": "^0.13.9", "rimraf": "^3.0.0", "rollup": "^2.75.5", + "rollup-plugin-execute": "^1.1.1", "rollup-plugin-terser": "^7.0.2", "typescript": "^3.9.7" }, diff --git a/scripts/__tests__/HorizonXText/StateManager/StateArray.test.js b/scripts/__tests__/HorizonXText/StateManager/StateArray.test.js deleted file mode 100644 index 826d6dda..00000000 --- a/scripts/__tests__/HorizonXText/StateManager/StateArray.test.js +++ /dev/null @@ -1,201 +0,0 @@ -import * as Horizon from '@cloudsop/horizon/index.ts'; -import { clearStore, createStore, useStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler'; -import { App, Text, triggerClickEvent } from '../../jest/commonComponents'; - -describe('测试store中的Array', () => { - const { unmountComponentAtNode } = Horizon; - let container = null; - beforeEach(() => { - // 创建一个 DOM 元素作为渲染目标 - container = document.createElement('div'); - document.body.appendChild(container); - - const persons = [ - { name: 'p1', age: 1 }, - { name: 'p2', age: 2 }, - ]; - - createStore({ - id: 'user', - state: { - type: 'bing dun dun', - persons: persons, - }, - actions: { - addOnePerson: (state, person) => { - state.persons.push(person); - }, - delOnePerson: state => { - state.persons.pop(); - }, - clearPersons: state => { - state.persons = null; - }, - }, - }); - }); - - afterEach(() => { - // 退出时进行清理 - unmountComponentAtNode(container); - container.remove(); - container = null; - - clearStore('user'); - }); - - const newPerson = { name: 'p3', age: 3 }; - - function Parent(props) { - const userStore = useStore('user'); - const addOnePerson = function() { - userStore.addOnePerson(newPerson); - }; - const delOnePerson = function() { - userStore.delOnePerson(); - }; - return ( -
- - -
{props.children}
-
- ); - } - - it('测试Array方法: push()、pop()', () => { - function Child(props) { - const userStore = useStore('user'); - - return ( -
- -
- ); - } - - Horizon.render(, container); - - expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: 2'); - // 在Array中增加一个对象 - Horizon.act(() => { - triggerClickEvent(container, 'addBtn'); - }); - expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: 3'); - - // 在Array中删除一个对象 - Horizon.act(() => { - triggerClickEvent(container, 'delBtn'); - }); - expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: 2'); - }); - - it('测试Array方法: entries()、push()、shift()、unshift、直接赋值', () => { - let globalStore = null; - - function Child(props) { - const userStore = useStore('user'); - globalStore = userStore; - - const nameList = []; - const entries = userStore.$state.persons?.entries(); - if (entries) { - for (const entry of entries) { - nameList.push(entry[1].name); - } - } - - return ( -
- -
- ); - } - - Horizon.render(, container); - - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2'); - // push - globalStore.$state.persons.push(newPerson); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3'); - - // shift - globalStore.$state.persons.shift({ name: 'p0', age: 0 }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p2 p3'); - - // 赋值[2] - globalStore.$state.persons[2] = { name: 'p4', age: 4 }; - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p2 p3 p4'); - - // 重新赋值[2] - globalStore.$state.persons[2] = { name: 'p5', age: 5 }; - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p2 p3 p5'); - - // unshift - globalStore.$state.persons.unshift({ name: 'p1', age: 1 }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3 p5'); - - // 重新赋值 null - globalStore.$state.persons = null; - expect(container.querySelector('#nameList').innerHTML).toBe('name list: '); - - // 重新赋值 [{ name: 'p1', age: 1 }] - globalStore.$state.persons = [{ name: 'p1', age: 1 }]; - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1'); - }); - - it('测试Array方法: forEach()', () => { - let globalStore = null; - - function Child(props) { - const userStore = useStore('user'); - globalStore = userStore; - - const nameList = []; - userStore.$state.persons?.forEach(per => { - nameList.push(per.name); - }); - - return ( -
- -
- ); - } - - Horizon.render(, container); - - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2'); - // push - globalStore.$state.persons.push(newPerson); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3'); - - // shift - globalStore.$state.persons.shift({ name: 'p0', age: 0 }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p2 p3'); - - // 赋值[2] - globalStore.$state.persons[2] = { name: 'p4', age: 4 }; - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p2 p3 p4'); - - // 重新赋值[2] - globalStore.$state.persons[2] = { name: 'p5', age: 5 }; - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p2 p3 p5'); - - // unshift - globalStore.$state.persons.unshift({ name: 'p1', age: 1 }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3 p5'); - - // 重新赋值 null - globalStore.$state.persons = null; - expect(container.querySelector('#nameList').innerHTML).toBe('name list: '); - - // 重新赋值 [{ name: 'p1', age: 1 }] - globalStore.$state.persons = [{ name: 'p1', age: 1 }]; - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1'); - }); -}); diff --git a/scripts/__tests__/HorizonXText/StateManager/StateArray.test.tsx b/scripts/__tests__/HorizonXText/StateManager/StateArray.test.tsx new file mode 100644 index 00000000..fbe1de0b --- /dev/null +++ b/scripts/__tests__/HorizonXText/StateManager/StateArray.test.tsx @@ -0,0 +1,208 @@ +//@ts-ignore +import * as Horizon from '@cloudsop/horizon/index.ts'; +import * as LogUtils from '../../jest/logUtils'; +import { clearStore, createStore, useStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler'; +import { App, Text, triggerClickEvent } from '../../jest/commonComponents'; +import { describe, beforeEach, afterEach, it, expect } from '@jest/globals'; + +const useUserStore = createStore({ + id: 'user', + state: { + type: 'bing dun dun', + persons: [ + { name: 'p1', age: 1 }, + { name: 'p2', age: 2 }, + ], + }, + actions: { + addOnePerson: (state, person) => { + state.persons.push(person); + }, + delOnePerson: state => { + state.persons.pop(); + }, + clearPersons: state => { + state.persons = []; + }, + reset: state => { + state.persons = [ + { name: 'p1', age: 1 }, + { name: 'p2', age: 2 }, + ]; + }, + }, +}); + +describe('测试store中的Array', () => { + const { unmountComponentAtNode } = Horizon; + let container: HTMLElement | null = null; + beforeEach(() => { + // 创建一个 DOM 元素作为渲染目标 + container = document.createElement('div'); + document.body.appendChild(container); + + useUserStore().reset(); + }); + + afterEach(() => { + // 退出时进行清理 + unmountComponentAtNode(container); + container?.remove(); + container = null; + LogUtils.clear(); + + clearStore('user'); + }); + + const newPerson = { name: 'p3', age: 3 }; + function Parent(props) { + const userStore = useUserStore(); + const addOnePerson = function() { + userStore.addOnePerson(newPerson); + }; + const delOnePerson = function() { + userStore.delOnePerson(); + }; + return ( +
+ + +
{props.children}
+
+ ); + } + + it('测试Array方法: push()、pop()', () => { + function Child(props) { + const userStore = useUserStore(); + + return ( +
+ +
+ ); + } + + Horizon.render(, container); + + expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: 2'); + // 在Array中增加一个对象 + Horizon.act(() => { + triggerClickEvent(container, 'addBtn'); + }); + expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: 3'); + + // 在Array中删除一个对象 + Horizon.act(() => { + triggerClickEvent(container, 'delBtn'); + }); + expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: 2'); + }); + + it('测试Array方法: entries()、push()、shift()、unshift、直接赋值', () => { + let globalStore = useUserStore(); + function Child(props) { + const userStore = useUserStore(); + + const nameList: string[] = []; + const entries = userStore.$s.persons?.entries(); + if (entries) { + for (const entry of entries) { + nameList.push(entry[1].name); + } + } + + return ( +
+ +
+ ); + } + + Horizon.render(, container); + + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2'); + // push + globalStore.$s.persons.push(newPerson); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2 p3'); + + // shift + //@ts-ignore TODO:why is this argument here? + globalStore.$s.persons.shift({ name: 'p0', age: 0 }); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p2 p3'); + + // 赋值[2] + globalStore.$s.persons[2] = { name: 'p4', age: 4 }; + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p2 p3 p4'); + + // 重新赋值[2] + globalStore.$s.persons[2] = { name: 'p5', age: 5 }; + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p2 p3 p5'); + + // unshift + globalStore.$s.persons.unshift({ name: 'p1', age: 1 }); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2 p3 p5'); + + // 重新赋值 [] + globalStore.$s.persons = []; + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: '); + + // 重新赋值 [{ name: 'p1', age: 1 }] + globalStore.$s.persons = [{ name: 'p1', age: 1 }]; + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1'); + }); + + it('测试Array方法: forEach()', () => { + let globalStore = useUserStore(); + function Child(props) { + const userStore = useUserStore(); + + const nameList: string[] = []; + userStore.$s.persons?.forEach(per => { + nameList.push(per.name); + }); + + return ( +
+ +
+ ); + } + + Horizon.render(, container); + + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2'); + // push + globalStore.$s.persons.push(newPerson); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2 p3'); + + // shift + //@ts-ignore TODO: why is this argument here? + globalStore.$s.persons.shift({ name: 'p0', age: 0 }); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p2 p3'); + + // 赋值[2] + globalStore.$s.persons[2] = { name: 'p4', age: 4 }; + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p2 p3 p4'); + + // 重新赋值[2] + globalStore.$s.persons[2] = { name: 'p5', age: 5 }; + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p2 p3 p5'); + + // unshift + globalStore.$s.persons.unshift({ name: 'p1', age: 1 }); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2 p3 p5'); + + // 重新赋值 [] + globalStore.$s.persons = []; + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: '); + + // 重新赋值 [{ name: 'p1', age: 1 }] + globalStore.$s.persons = [{ name: 'p1', age: 1 }]; + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1'); + }); +}); diff --git a/scripts/__tests__/HorizonXText/StateManager/StateMap.test.js b/scripts/__tests__/HorizonXText/StateManager/StateMap.test.tsx similarity index 60% rename from scripts/__tests__/HorizonXText/StateManager/StateMap.test.js rename to scripts/__tests__/HorizonXText/StateManager/StateMap.test.tsx index f7eaed9f..e5d1b141 100644 --- a/scripts/__tests__/HorizonXText/StateManager/StateMap.test.js +++ b/scripts/__tests__/HorizonXText/StateManager/StateMap.test.tsx @@ -1,45 +1,55 @@ +//@ts-ignore import * as Horizon from '@cloudsop/horizon/index.ts'; +import * as LogUtils from '../../jest/logUtils'; import { clearStore, createStore, useStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler'; import { App, Text, triggerClickEvent } from '../../jest/commonComponents'; +import { describe, beforeEach, afterEach, it, expect } from '@jest/globals'; + +const useUserStore = createStore({ + id: 'user', + state: { + type: 'bing dun dun', + persons: new Map([ + ['p1', 1], + ['p2', 2], + ]), + }, + actions: { + addOnePerson: (state, person) => { + state.persons.set(person.name, person.age); + }, + delOnePerson: (state, person) => { + state.persons.delete(person.name); + }, + clearPersons: state => { + state.persons.clear(); + }, + reset: state => { + state.persons = new Map([ + ['p1', 1], + ['p2', 2], + ]); + }, + }, +}); describe('测试store中的Map', () => { const { unmountComponentAtNode } = Horizon; - let container = null; + let container: HTMLElement | null = null; beforeEach(() => { // 创建一个 DOM 元素作为渲染目标 container = document.createElement('div'); document.body.appendChild(container); - const persons = new Map([ - ['p1', 1], - ['p2', 2], - ]); - - createStore({ - id: 'user', - state: { - type: 'bing dun dun', - persons: persons, - }, - actions: { - addOnePerson: (state, person) => { - state.persons.set(person.name, person.age); - }, - delOnePerson: (state, person) => { - state.persons.delete(person.name); - }, - clearPersons: state => { - state.persons.clear(); - }, - }, - }); + useUserStore().reset(); }); afterEach(() => { // 退出时进行清理 unmountComponentAtNode(container); - container.remove(); + container?.remove(); container = null; + LogUtils.clear(); clearStore('user'); }); @@ -47,7 +57,7 @@ describe('测试store中的Map', () => { const newPerson = { name: 'p3', age: 3 }; function Parent(props) { - const userStore = useStore('user'); + const userStore = useUserStore(); const addOnePerson = function() { userStore.addOnePerson(newPerson); }; @@ -76,43 +86,43 @@ describe('测试store中的Map', () => { it('测试Map方法: set()、delete()、clear()', () => { function Child(props) { - const userStore = useStore('user'); + const userStore = useUserStore(); return (
- +
); } Horizon.render(, container); - expect(container.querySelector('#size').innerHTML).toBe('persons number: 2'); + expect(container?.querySelector('#size')?.innerHTML).toBe('persons number: 2'); // 在Map中增加一个对象 Horizon.act(() => { triggerClickEvent(container, 'addBtn'); }); - expect(container.querySelector('#size').innerHTML).toBe('persons number: 3'); + expect(container?.querySelector('#size')?.innerHTML).toBe('persons number: 3'); // 在Map中删除一个对象 Horizon.act(() => { triggerClickEvent(container, 'delBtn'); }); - expect(container.querySelector('#size').innerHTML).toBe('persons number: 2'); + expect(container?.querySelector('#size')?.innerHTML).toBe('persons number: 2'); // clear Map Horizon.act(() => { triggerClickEvent(container, 'clearBtn'); }); - expect(container.querySelector('#size').innerHTML).toBe('persons number: 0'); + expect(container?.querySelector('#size')?.innerHTML).toBe('persons number: 0'); }); it('测试Map方法: keys()', () => { function Child(props) { - const userStore = useStore('user'); + const userStore = useUserStore(); - const nameList = []; - const keys = userStore.$state.persons.keys(); + const nameList: string[] = []; + const keys = userStore.$s.persons.keys(); for (const key of keys) { nameList.push(key); } @@ -126,32 +136,32 @@ describe('测试store中的Map', () => { Horizon.render(, container); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2'); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2'); // 在Map中增加一个对象 Horizon.act(() => { triggerClickEvent(container, 'addBtn'); }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3'); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2 p3'); // 在Map中删除一个对象 Horizon.act(() => { triggerClickEvent(container, 'delBtn'); }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2'); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2'); // clear Map Horizon.act(() => { triggerClickEvent(container, 'clearBtn'); }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: '); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: '); }); it('测试Map方法: values()', () => { function Child(props) { - const userStore = useStore('user'); + const userStore = useUserStore(); - const ageList = []; - const values = userStore.$state.persons.values(); + const ageList: number[] = []; + const values = userStore.$s.persons.values(); for (const val of values) { ageList.push(val); } @@ -165,32 +175,32 @@ describe('测试store中的Map', () => { Horizon.render(, container); - expect(container.querySelector('#ageList').innerHTML).toBe('age list: 1 2'); + expect(container?.querySelector('#ageList')?.innerHTML).toBe('age list: 1 2'); // 在Map中增加一个对象 Horizon.act(() => { triggerClickEvent(container, 'addBtn'); }); - expect(container.querySelector('#ageList').innerHTML).toBe('age list: 1 2 3'); + expect(container?.querySelector('#ageList')?.innerHTML).toBe('age list: 1 2 3'); // 在Map中删除一个对象 Horizon.act(() => { triggerClickEvent(container, 'delBtn'); }); - expect(container.querySelector('#ageList').innerHTML).toBe('age list: 1 2'); + expect(container?.querySelector('#ageList')?.innerHTML).toBe('age list: 1 2'); // clear Map Horizon.act(() => { triggerClickEvent(container, 'clearBtn'); }); - expect(container.querySelector('#ageList').innerHTML).toBe('age list: '); + expect(container?.querySelector('#ageList')?.innerHTML).toBe('age list: '); }); it('测试Map方法: entries()', () => { function Child(props) { - const userStore = useStore('user'); + const userStore = useUserStore(); - const nameList = []; - const entries = userStore.$state.persons.entries(); + const nameList: string[] = []; + const entries = userStore.$s.persons.entries(); for (const entry of entries) { nameList.push(entry[0]); } @@ -204,32 +214,32 @@ describe('测试store中的Map', () => { Horizon.render(, container); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2'); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2'); // 在Map中增加一个对象 Horizon.act(() => { triggerClickEvent(container, 'addBtn'); }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3'); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2 p3'); // 在Map中删除一个对象 Horizon.act(() => { triggerClickEvent(container, 'delBtn'); }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2'); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2'); // clear Map Horizon.act(() => { triggerClickEvent(container, 'clearBtn'); }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: '); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: '); }); it('测试Map方法: forEach()', () => { function Child(props) { - const userStore = useStore('user'); + const userStore = useUserStore(); - const nameList = []; - userStore.$state.persons.forEach((val, key) => { + const nameList: string[] = []; + userStore.$s.persons.forEach((val, key) => { nameList.push(key); }); @@ -242,53 +252,53 @@ describe('测试store中的Map', () => { Horizon.render(, container); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2'); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2'); // 在Map中增加一个对象 Horizon.act(() => { triggerClickEvent(container, 'addBtn'); }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3'); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2 p3'); // 在Map中删除一个对象 Horizon.act(() => { triggerClickEvent(container, 'delBtn'); }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2'); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2'); // clear Map Horizon.act(() => { triggerClickEvent(container, 'clearBtn'); }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: '); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: '); }); it('测试Map方法: has()', () => { function Child(props) { - const userStore = useStore('user'); + const userStore = useUserStore(); return (
- +
); } Horizon.render(, container); - expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: false'); + expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: false'); // 在Map中增加一个对象 Horizon.act(() => { triggerClickEvent(container, 'addBtn'); }); - expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: true'); + expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: true'); }); it('测试Map方法: for of()', () => { function Child(props) { - const userStore = useStore('user'); + const userStore = useUserStore(); - const nameList = []; - for (const per of userStore.$state.persons) { + const nameList: string[] = []; + for (const per of userStore.$s.persons) { nameList.push(per[0]); } @@ -301,23 +311,23 @@ describe('测试store中的Map', () => { Horizon.render(, container); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2'); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2'); // 在Map中增加一个对象 Horizon.act(() => { triggerClickEvent(container, 'addBtn'); }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3'); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2 p3'); // 在Map中删除一个对象 Horizon.act(() => { triggerClickEvent(container, 'delBtn'); }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2'); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2'); // clear Map Horizon.act(() => { triggerClickEvent(container, 'clearBtn'); }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: '); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: '); }); }); diff --git a/scripts/__tests__/HorizonXText/StateManager/StateMixType.test.js b/scripts/__tests__/HorizonXText/StateManager/StateMixType.test.tsx similarity index 77% rename from scripts/__tests__/HorizonXText/StateManager/StateMixType.test.js rename to scripts/__tests__/HorizonXText/StateManager/StateMixType.test.tsx index 870d7d26..bb38773d 100644 --- a/scripts/__tests__/HorizonXText/StateManager/StateMixType.test.js +++ b/scripts/__tests__/HorizonXText/StateManager/StateMixType.test.tsx @@ -1,10 +1,12 @@ +//@ts-ignore import * as Horizon from '@cloudsop/horizon/index.ts'; +import * as LogUtils from '../../jest/logUtils'; import { clearStore, createStore, useStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler'; import { App, Text, triggerClickEvent } from '../../jest/commonComponents'; describe('测试store中的混合类型变化', () => { const { unmountComponentAtNode } = Horizon; - let container = null; + let container: HTMLElement | null = null; beforeEach(() => { // 创建一个 DOM 元素作为渲染目标 container = document.createElement('div'); @@ -42,8 +44,9 @@ describe('测试store中的混合类型变化', () => { afterEach(() => { // 退出时进行清理 unmountComponentAtNode(container); - container.remove(); + (container as HTMLElement).remove(); container = null; + LogUtils.clear(); clearStore('user'); }); @@ -68,7 +71,7 @@ describe('测试store中的混合类型变化', () => { function Child(props) { const userStore = useStore('user'); - const days = userStore.$state.persons + const days = userStore.persons .values() .next() .value.love.get('lanqiu').days; @@ -82,11 +85,11 @@ describe('测试store中的混合类型变化', () => { Horizon.render(, container); - expect(container.querySelector('#dayList').innerHTML).toBe('love: 1 3 5'); + expect(container?.querySelector('#dayList')?.innerHTML).toBe('love: 1 3 5'); Horizon.act(() => { triggerClickEvent(container, 'addBtn'); }); - expect(container.querySelector('#dayList').innerHTML).toBe('love: 1 3 5 7'); + expect(container?.querySelector('#dayList')?.innerHTML).toBe('love: 1 3 5 7'); }); it('属性是个class实例', () => { @@ -103,7 +106,6 @@ describe('测试store中的混合类型变化', () => { setName(name) { this.name = name; } - getName() { return this.name; } @@ -111,7 +113,6 @@ describe('测试store中的混合类型变化', () => { setAge(age) { this.age = age; } - getAge() { return this.age; } @@ -119,7 +120,6 @@ describe('测试store中的混合类型变化', () => { addLove(lv) { this.loves.add(lv); } - getLoves() { return this.loves; } @@ -127,14 +127,19 @@ describe('测试store中的混合类型变化', () => { let globalPerson; let globalStore; - function Child(props) { const userStore = useStore('user'); globalStore = userStore; - const nameList = []; - const valIterator = userStore.$state.persons.values(); - let per = valIterator.next(); + const nameList: string[] = []; + const valIterator = userStore.persons.values(); + let per = valIterator.next() as { + value: { + name: string; + getName: () => string; + }; + done: boolean; + }; while (!per.done) { nameList.push(per.value.name ?? per.value.getName()); globalPerson = per.value; @@ -150,15 +155,15 @@ describe('测试store中的混合类型变化', () => { Horizon.render(, container); - expect(container.querySelector('#nameList').innerHTML).toBe('p1 p2'); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('p1 p2'); // 动态增加一个Person实例 - globalStore.$state.persons.add(new Person('ClassPerson', 5)); + globalStore.$s.persons.add(new Person('ClassPerson', 5)); - expect(container.querySelector('#nameList').innerHTML).toBe('p1 p2 ClassPerson'); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('p1 p2 ClassPerson'); globalPerson.setName('ClassPerson1'); - expect(container.querySelector('#nameList').innerHTML).toBe('p1 p2 ClassPerson1'); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('p1 p2 ClassPerson1'); }); }); diff --git a/scripts/__tests__/HorizonXText/StateManager/StateSet.test.js b/scripts/__tests__/HorizonXText/StateManager/StateSet.test.tsx similarity index 58% rename from scripts/__tests__/HorizonXText/StateManager/StateSet.test.js rename to scripts/__tests__/HorizonXText/StateManager/StateSet.test.tsx index 4b6bdc7b..a1a245f0 100644 --- a/scripts/__tests__/HorizonXText/StateManager/StateSet.test.js +++ b/scripts/__tests__/HorizonXText/StateManager/StateSet.test.tsx @@ -1,53 +1,62 @@ +//@ts-ignore import * as Horizon from '@cloudsop/horizon/index.ts'; +import * as LogUtils from '../../jest/logUtils'; import { clearStore, createStore, useStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler'; import { App, Text, triggerClickEvent } from '../../jest/commonComponents'; +import { describe, beforeEach, afterEach, it, expect } from '@jest/globals'; + +const useUserStore = createStore({ + id: 'user', + state: { + type: 'bing dun dun', + persons: new Set([ + { name: 'p1', age: 1 }, + { name: 'p2', age: 2 }, + ]), + }, + actions: { + addOnePerson: (state, person) => { + state.persons.add(person); + }, + delOnePerson: (state, person) => { + state.persons.delete(person); + }, + clearPersons: state => { + state.persons.clear(); + }, + reset: state => { + state.persons = new Set([ + { name: 'p1', age: 1 }, + { name: 'p2', age: 2 }, + ]); + }, + }, +}); describe('测试store中的Set', () => { const { unmountComponentAtNode } = Horizon; - let container = null; + let container: HTMLElement | null = null; beforeEach(() => { // 创建一个 DOM 元素作为渲染目标 container = document.createElement('div'); document.body.appendChild(container); - const persons = new Set([ - { name: 'p1', age: 1 }, - { name: 'p2', age: 2 }, - ]); - - createStore({ - id: 'user', - state: { - type: 'bing dun dun', - persons: persons, - }, - actions: { - addOnePerson: (state, person) => { - state.persons.add(person); - }, - delOnePerson: (state, person) => { - state.persons.delete(person); - }, - clearPersons: state => { - state.persons.clear(); - }, - }, - }); + useUserStore().reset(); }); afterEach(() => { // 退出时进行清理 unmountComponentAtNode(container); - container.remove(); + container?.remove(); container = null; + LogUtils.clear(); clearStore('user'); }); const newPerson = { name: 'p3', age: 3 }; - function Parent(props) { - const userStore = useStore('user'); + const userStore = useUserStore(); const addOnePerson = function() { userStore.addOnePerson(newPerson); }; @@ -76,17 +85,17 @@ describe('测试store中的Set', () => { it('测试Set方法: add()、delete()、clear()', () => { function Child(props) { - const userStore = useStore('user'); - const personArr = Array.from(userStore.$state.persons); - const nameList = []; - const keys = userStore.$state.persons.keys(); + const userStore = useUserStore(); + const personArr = Array.from(userStore.$s.persons); + const nameList: string[] = []; + const keys = userStore.$s.persons.keys(); for (const key of keys) { nameList.push(key.name); } return (
- +
); @@ -94,35 +103,35 @@ describe('测试store中的Set', () => { Horizon.render(, container); - expect(container.querySelector('#size').innerHTML).toBe('persons number: 2'); - expect(container.querySelector('#lastAge').innerHTML).toBe('last person age: 2'); + expect(container?.querySelector('#size')?.innerHTML).toBe('persons number: 2'); + expect(container?.querySelector('#lastAge')?.innerHTML).toBe('last person age: 2'); // 在set中增加一个对象 Horizon.act(() => { triggerClickEvent(container, 'addBtn'); }); - expect(container.querySelector('#size').innerHTML).toBe('persons number: 3'); + expect(container?.querySelector('#size')?.innerHTML).toBe('persons number: 3'); // 在set中删除一个对象 Horizon.act(() => { triggerClickEvent(container, 'delBtn'); }); - expect(container.querySelector('#size').innerHTML).toBe('persons number: 2'); + expect(container?.querySelector('#size')?.innerHTML).toBe('persons number: 2'); // clear set Horizon.act(() => { triggerClickEvent(container, 'clearBtn'); }); - expect(container.querySelector('#size').innerHTML).toBe('persons number: 0'); - expect(container.querySelector('#lastAge').innerHTML).toBe('last person age: 0'); + expect(container?.querySelector('#size')?.innerHTML).toBe('persons number: 0'); + expect(container?.querySelector('#lastAge')?.innerHTML).toBe('last person age: 0'); }); it('测试Set方法: keys()、values()', () => { function Child(props) { - const userStore = useStore('user'); + const userStore = useUserStore(); - const nameList = []; - const keys = userStore.$state.persons.keys(); - // const keys = userStore.$state.persons.values(); + const nameList: string[] = []; + const keys = userStore.$s.persons.keys(); + // const keys = userStore.$s.persons.values(); for (const key of keys) { nameList.push(key.name); } @@ -136,32 +145,32 @@ describe('测试store中的Set', () => { Horizon.render(, container); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2'); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2'); // 在set中增加一个对象 Horizon.act(() => { triggerClickEvent(container, 'addBtn'); }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3'); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2 p3'); // 在set中删除一个对象 Horizon.act(() => { triggerClickEvent(container, 'delBtn'); }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2'); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2'); // clear set Horizon.act(() => { triggerClickEvent(container, 'clearBtn'); }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: '); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: '); }); it('测试Set方法: entries()', () => { function Child(props) { - const userStore = useStore('user'); + const userStore = useUserStore(); - const nameList = []; - const entries = userStore.$state.persons.entries(); + const nameList: string[] = []; + const entries = userStore.$s.persons.entries(); for (const entry of entries) { nameList.push(entry[0].name); } @@ -175,32 +184,32 @@ describe('测试store中的Set', () => { Horizon.render(, container); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2'); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2'); // 在set中增加一个对象 Horizon.act(() => { triggerClickEvent(container, 'addBtn'); }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3'); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2 p3'); // 在set中删除一个对象 Horizon.act(() => { triggerClickEvent(container, 'delBtn'); }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2'); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2'); // clear set Horizon.act(() => { triggerClickEvent(container, 'clearBtn'); }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: '); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: '); }); it('测试Set方法: forEach()', () => { function Child(props) { - const userStore = useStore('user'); + const userStore = useUserStore(); - const nameList = []; - userStore.$state.persons.forEach(per => { + const nameList: string[] = []; + userStore.$s.persons.forEach(per => { nameList.push(per.name); }); @@ -213,53 +222,53 @@ describe('测试store中的Set', () => { Horizon.render(, container); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2'); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2'); // 在set中增加一个对象 Horizon.act(() => { triggerClickEvent(container, 'addBtn'); }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3'); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2 p3'); // 在set中删除一个对象 Horizon.act(() => { triggerClickEvent(container, 'delBtn'); }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2'); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2'); // clear set Horizon.act(() => { triggerClickEvent(container, 'clearBtn'); }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: '); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: '); }); it('测试Set方法: has()', () => { function Child(props) { - const userStore = useStore('user'); + const userStore = useUserStore(); return (
- +
); } Horizon.render(, container); - expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: false'); + expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: false'); // 在set中增加一个对象 Horizon.act(() => { triggerClickEvent(container, 'addBtn'); }); - expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: true'); + expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: true'); }); it('测试Set方法: for of()', () => { function Child(props) { - const userStore = useStore('user'); + const userStore = useUserStore(); - const nameList = []; - for (const per of userStore.$state.persons) { + const nameList: string[] = []; + for (const per of userStore.$s.persons) { nameList.push(per.name); } @@ -272,23 +281,23 @@ describe('测试store中的Set', () => { Horizon.render(, container); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2'); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2'); // 在set中增加一个对象 Horizon.act(() => { triggerClickEvent(container, 'addBtn'); }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3'); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2 p3'); // 在set中删除一个对象 Horizon.act(() => { triggerClickEvent(container, 'delBtn'); }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2'); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2'); // clear set Horizon.act(() => { triggerClickEvent(container, 'clearBtn'); }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: '); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: '); }); }); diff --git a/scripts/__tests__/HorizonXText/StateManager/StateWeakMap.test.js b/scripts/__tests__/HorizonXText/StateManager/StateWeakMap.test.tsx similarity index 61% rename from scripts/__tests__/HorizonXText/StateManager/StateWeakMap.test.js rename to scripts/__tests__/HorizonXText/StateManager/StateWeakMap.test.tsx index 28d4ff98..2d5a441f 100644 --- a/scripts/__tests__/HorizonXText/StateManager/StateWeakMap.test.js +++ b/scripts/__tests__/HorizonXText/StateManager/StateWeakMap.test.tsx @@ -1,45 +1,55 @@ +//@ts-ignore import * as Horizon from '@cloudsop/horizon/index.ts'; +import * as LogUtils from '../../jest/logUtils'; import { clearStore, createStore, useStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler'; import { App, Text, triggerClickEvent } from '../../jest/commonComponents'; +import { describe, beforeEach, afterEach, it, expect } from '@jest/globals'; + +const useUserStore = createStore({ + id: 'user', + state: { + type: 'bing dun dun', + persons: new WeakMap([ + [{ name: 'p1' }, 1], + [{ name: 'p2' }, 2], + ]), + }, + actions: { + addOnePerson: (state, person) => { + state.persons.set(person, 3); + }, + delOnePerson: (state, person) => { + state.persons.delete(person); + }, + clearPersons: state => { + state.persons = new WeakMap([]); + }, + reset: state => { + state.persons = new WeakMap([ + [{ name: 'p1' }, 1], + [{ name: 'p2' }, 2], + ]); + }, + }, +}); describe('测试store中的WeakMap', () => { const { unmountComponentAtNode } = Horizon; - let container = null; + let container: HTMLElement | null = null; beforeEach(() => { // 创建一个 DOM 元素作为渲染目标 container = document.createElement('div'); document.body.appendChild(container); - const persons = new WeakMap([ - [{ name: 'p1' }, 1], - [{ name: 'p2' }, 2], - ]); - - createStore({ - id: 'user', - state: { - type: 'bing dun dun', - persons: persons, - }, - actions: { - addOnePerson: (state, person) => { - state.persons.set(person, 3); - }, - delOnePerson: (state, person) => { - state.persons.delete(person); - }, - clearPersons: state => { - state.persons.clear(); - }, - }, - }); + useUserStore().reset(); }); afterEach(() => { // 退出时进行清理 unmountComponentAtNode(container); - container.remove(); + container?.remove(); container = null; + LogUtils.clear(); clearStore('user'); }); @@ -47,7 +57,7 @@ describe('测试store中的WeakMap', () => { const newPerson = { name: 'p3' }; function Parent(props) { - const userStore = useStore('user'); + const userStore = useUserStore(); const addOnePerson = function() { userStore.addOnePerson(newPerson); }; @@ -76,49 +86,49 @@ describe('测试store中的WeakMap', () => { it('测试WeakMap方法: set()、delete()、has()', () => { function Child(props) { - const userStore = useStore('user'); + const userStore = useUserStore(); return (
- +
); } Horizon.render(, container); - expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: false'); + expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: false'); // 在WeakMap中增加一个对象 Horizon.act(() => { triggerClickEvent(container, 'addBtn'); }); - expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: true'); + expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: true'); // 在WeakMap中删除一个对象 Horizon.act(() => { triggerClickEvent(container, 'delBtn'); }); - expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: false'); + expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: false'); }); it('测试WeakMap方法: get()', () => { function Child(props) { - const userStore = useStore('user'); + const userStore = useUserStore(); return (
- +
); } Horizon.render(, container); - expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: undefined'); + expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: undefined'); // 在WeakMap中增加一个对象 Horizon.act(() => { triggerClickEvent(container, 'addBtn'); }); - expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: 3'); + expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: 3'); }); }); diff --git a/scripts/__tests__/HorizonXText/StateManager/StateWeakSet.test.js b/scripts/__tests__/HorizonXText/StateManager/StateWeakSet.test.tsx similarity index 59% rename from scripts/__tests__/HorizonXText/StateManager/StateWeakSet.test.js rename to scripts/__tests__/HorizonXText/StateManager/StateWeakSet.test.tsx index 4c6a8fca..ecbc632d 100644 --- a/scripts/__tests__/HorizonXText/StateManager/StateWeakSet.test.js +++ b/scripts/__tests__/HorizonXText/StateManager/StateWeakSet.test.tsx @@ -1,53 +1,62 @@ +//@ts-ignore import * as Horizon from '@cloudsop/horizon/index.ts'; +import * as LogUtils from '../../jest/logUtils'; import { clearStore, createStore, useStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler'; import { App, Text, triggerClickEvent } from '../../jest/commonComponents'; +import { describe, beforeEach, afterEach, it, expect } from '@jest/globals'; + +const useUserStore = createStore({ + id: 'user', + state: { + type: 'bing dun dun', + persons: new WeakSet([ + { name: 'p1', age: 1 }, + { name: 'p2', age: 2 }, + ]), + }, + actions: { + addOnePerson: (state, person) => { + state.persons.add(person); + }, + delOnePerson: (state, person) => { + state.persons.delete(person); + }, + clearPersons: state => { + state.persons = new WeakSet([]); + }, + reset: state => { + state.persons = new WeakSet([ + { name: 'p1', age: 1 }, + { name: 'p2', age: 2 }, + ]); + }, + }, +}); describe('测试store中的WeakSet', () => { const { unmountComponentAtNode } = Horizon; - let container = null; + let container: HTMLElement | null = null; beforeEach(() => { // 创建一个 DOM 元素作为渲染目标 container = document.createElement('div'); document.body.appendChild(container); - const persons = new WeakSet([ - { name: 'p1', age: 1 }, - { name: 'p2', age: 2 }, - ]); - - createStore({ - id: 'user', - state: { - type: 'bing dun dun', - persons: persons, - }, - actions: { - addOnePerson: (state, person) => { - state.persons.add(person); - }, - delOnePerson: (state, person) => { - state.persons.delete(person); - }, - clearPersons: state => { - state.persons.clear(); - }, - }, - }); + useUserStore().reset(); }); afterEach(() => { // 退出时进行清理 unmountComponentAtNode(container); - container.remove(); + container?.remove(); container = null; + LogUtils.clear(); clearStore('user'); }); const newPerson = { name: 'p3', age: 3 }; - function Parent(props) { - const userStore = useStore('user'); + const userStore = useUserStore(); const addOnePerson = function() { userStore.addOnePerson(newPerson); }; @@ -69,28 +78,28 @@ describe('测试store中的WeakSet', () => { it('测试WeakSet方法: add()、delete()、has()', () => { function Child(props) { - const userStore = useStore('user'); + const userStore = useUserStore(); return (
- +
); } Horizon.render(, container); - expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: false'); + expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: false'); // 在WeakSet中增加一个对象 Horizon.act(() => { triggerClickEvent(container, 'addBtn'); }); - expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: true'); + expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: true'); // 在WeakSet中删除一个对象 Horizon.act(() => { triggerClickEvent(container, 'delBtn'); }); - expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: false'); + expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: false'); }); }); diff --git a/scripts/__tests__/HorizonXText/StoreFunctionality/async.test.js b/scripts/__tests__/HorizonXText/StoreFunctionality/async.test.tsx similarity index 80% rename from scripts/__tests__/HorizonXText/StoreFunctionality/async.test.js rename to scripts/__tests__/HorizonXText/StoreFunctionality/async.test.tsx index f45599d2..66c287e9 100644 --- a/scripts/__tests__/HorizonXText/StoreFunctionality/async.test.js +++ b/scripts/__tests__/HorizonXText/StoreFunctionality/async.test.tsx @@ -1,6 +1,8 @@ +//@ts-ignore import * as Horizon from '@cloudsop/horizon/index.ts'; import { createStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler'; import { triggerClickEvent } from '../../jest/commonComponents'; +import { describe, beforeEach, afterEach, it, expect } from '@jest/globals'; const { unmountComponentAtNode } = Horizon; @@ -13,7 +15,7 @@ function postpone(timer, func) { } describe('Asynchronous functions', () => { - let container = null; + let container: HTMLElement | null = null; const COUNTER_ID = 'counter'; const TOGGLE_ID = 'toggle'; @@ -33,7 +35,7 @@ describe('Asynchronous functions', () => { return new Promise(resolve => { setTimeout(() => { state.counter++; - resolve(); + resolve(true); }, 100); }); }, @@ -53,11 +55,12 @@ describe('Asynchronous functions', () => { afterEach(() => { unmountComponentAtNode(container); - container.remove(); + container?.remove(); container = null; }); it('Should wait for async actions', async () => { + // @ts-ignore jest.useRealTimers(); let globalStore; @@ -84,14 +87,14 @@ describe('Asynchronous functions', () => { Horizon.render(, container); // initial state - expect(document.getElementById(RESULT_ID).innerHTML).toBe('false0'); + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('false0'); // slow toggle has nothing to wait for, it is resolved immediately Horizon.act(() => { triggerClickEvent(container, TOGGLE_ID); }); - expect(document.getElementById(RESULT_ID).innerHTML).toBe('true0'); + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('true0'); // counter increment is slow. slow toggle waits for result Horizon.act(() => { @@ -101,18 +104,18 @@ describe('Asynchronous functions', () => { triggerClickEvent(container, TOGGLE_ID); }); - expect(document.getElementById(RESULT_ID).innerHTML).toBe('true0'); + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('true0'); // fast toggle does not wait for counter and it is resolved immediately Horizon.act(() => { triggerClickEvent(container, TOGGLE_FAST_ID); }); - expect(document.getElementById(RESULT_ID).innerHTML).toBe('false0'); + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('false0'); // at 150ms counter increment will be resolved and slow toggle immediately after const t150 = postpone(150, () => { - expect(document.getElementById(RESULT_ID).innerHTML).toBe('true1'); + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('true1'); }); // before that, two more actions are added to queue - another counter and slow toggle @@ -125,13 +128,14 @@ describe('Asynchronous functions', () => { // at 250ms they should be already resolved const t250 = postpone(250, () => { - expect(document.getElementById(RESULT_ID).innerHTML).toBe('false2'); + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('false2'); }); await Promise.all([t150, t250]); }); it('call async action by then', async () => { + // @ts-ignore jest.useFakeTimers(); let globalStore; @@ -150,12 +154,13 @@ describe('Asynchronous functions', () => { // call async action by then globalStore.$queue.increment().then(() => { - expect(document.getElementById(RESULT_ID).innerHTML).toBe('false1'); + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('false1'); }); - expect(document.getElementById(RESULT_ID).innerHTML).toBe('false0'); + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('false0'); // past 150 ms + // @ts-ignore jest.advanceTimersByTime(150); }); }); diff --git a/scripts/__tests__/HorizonXText/StoreFunctionality/basicAccess.test.js b/scripts/__tests__/HorizonXText/StoreFunctionality/basicAccess.test.js deleted file mode 100644 index e42feeda..00000000 --- a/scripts/__tests__/HorizonXText/StoreFunctionality/basicAccess.test.js +++ /dev/null @@ -1,63 +0,0 @@ -import * as Horizon from '@cloudsop/horizon/index.ts'; -import { triggerClickEvent } from '../../jest/commonComponents'; -import { useLogStore } from './store'; - -const { unmountComponentAtNode } = Horizon; - -describe('Basic store manipulation', () => { - let container = null; - - const BUTTON_ID = 'btn'; - const RESULT_ID = 'result'; - - beforeEach(() => { - container = document.createElement('div'); - document.body.appendChild(container); - }); - - afterEach(() => { - unmountComponentAtNode(container); - container.remove(); - container = null; - }); - - it('Should use getters', () => { - function App() { - const logStore = useLogStore(); - - return
{logStore.length}
; - } - - Horizon.render(, container); - - expect(document.getElementById(RESULT_ID).innerHTML).toBe('1'); - }); - - it('Should use actions and update components', () => { - function App() { - const logStore = useLogStore(); - - return ( -
- -

{logStore.length}

-
- ); - } - - Horizon.render(, container); - - Horizon.act(() => { - triggerClickEvent(container, BUTTON_ID); - }); - - expect(document.getElementById(RESULT_ID).innerHTML).toBe('2'); - }); -}); diff --git a/scripts/__tests__/HorizonXText/StoreFunctionality/basicAccess.test.tsx b/scripts/__tests__/HorizonXText/StoreFunctionality/basicAccess.test.tsx new file mode 100644 index 00000000..0a2c8584 --- /dev/null +++ b/scripts/__tests__/HorizonXText/StoreFunctionality/basicAccess.test.tsx @@ -0,0 +1,110 @@ +//@ts-ignore +import Horizon from '@cloudsop/horizon/index.ts'; +import { triggerClickEvent } from '../../jest/commonComponents'; +import { useLogStore } from './store'; +import { describe, beforeEach, afterEach, it, expect } from '@jest/globals'; +import { createStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler'; + +const { unmountComponentAtNode } = Horizon; + +describe('Basic store manipulation', () => { + let container: HTMLElement | null = null; + + const BUTTON_ID = 'btn'; + const RESULT_ID = 'result'; + + beforeEach(() => { + container = document.createElement('div'); + document.body.appendChild(container); + }); + + afterEach(() => { + unmountComponentAtNode(container); + container?.remove(); + container = null; + }); + + it('Should use getters', () => { + function App() { + const logStore = useLogStore(); + + return
{logStore.length}
; + } + + Horizon.render(, container); + + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('1'); + }); + + it('Should use actions and update components', () => { + function App() { + const logStore = useLogStore(); + + return ( +
+ +

{logStore.length}

+
+ ); + } + + Horizon.render(, container); + + Horizon.act(() => { + triggerClickEvent(container, BUTTON_ID); + }); + + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('2'); + }); + + it('should call actions from own actions', () => { + const useIncrementStore = createStore({ + id: 'incrementStore', + state: { + count: 2, + }, + actions: { + increment: state => { + state.count++; + }, + doublePlusOne: function(state) { + state.count = state.count * 2; + this.increment(); + }, + }, + }); + + function App() { + const incrementStore = useIncrementStore(); + + return ( +
+ +

{incrementStore.count}

+
+ ); + } + + Horizon.render(, container); + + Horizon.act(() => { + triggerClickEvent(container, BUTTON_ID); + }); + + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('5'); + }); +}); diff --git a/scripts/__tests__/HorizonXText/StoreFunctionality/dollarAccess.test.js b/scripts/__tests__/HorizonXText/StoreFunctionality/dollarAccess.test.tsx similarity index 64% rename from scripts/__tests__/HorizonXText/StoreFunctionality/dollarAccess.test.js rename to scripts/__tests__/HorizonXText/StoreFunctionality/dollarAccess.test.tsx index 032234d0..94d149f0 100644 --- a/scripts/__tests__/HorizonXText/StoreFunctionality/dollarAccess.test.js +++ b/scripts/__tests__/HorizonXText/StoreFunctionality/dollarAccess.test.tsx @@ -1,11 +1,13 @@ +//@ts-ignore import * as Horizon from '@cloudsop/horizon/index.ts'; import { triggerClickEvent } from '../../jest/commonComponents'; import { useLogStore } from './store'; +import { describe, beforeEach, afterEach, it, expect } from '@jest/globals'; const { unmountComponentAtNode } = Horizon; describe('Dollar store access', () => { - let container = null; + let container: HTMLElement | null = null; const BUTTON_ID = 'btn'; const RESULT_ID = 'result'; @@ -17,23 +19,23 @@ describe('Dollar store access', () => { afterEach(() => { unmountComponentAtNode(container); - container.remove(); + container?.remove(); container = null; }); - it('Should use $state and $computed', () => { + it('Should use $s and $c', () => { function App() { const logStore = useLogStore(); - return
{logStore.$computed.length()}
; + return
{logStore.$c.length()}
; } Horizon.render(, container); - expect(document.getElementById(RESULT_ID).innerHTML).toBe('1'); + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('1'); }); - it('Should use $actions and update components', () => { + it('Should use $a and update components', () => { function App() { const logStore = useLogStore(); @@ -42,12 +44,12 @@ describe('Dollar store access', () => { -

{logStore.$computed.length()}

+

{logStore.$c.length()}

); } @@ -58,6 +60,6 @@ describe('Dollar store access', () => { triggerClickEvent(container, BUTTON_ID); }); - expect(document.getElementById(RESULT_ID).innerHTML).toBe('2'); + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('2'); }); }); diff --git a/scripts/__tests__/HorizonXText/StoreFunctionality/otherCases.test.js b/scripts/__tests__/HorizonXText/StoreFunctionality/otherCases.test.tsx similarity index 71% rename from scripts/__tests__/HorizonXText/StoreFunctionality/otherCases.test.js rename to scripts/__tests__/HorizonXText/StoreFunctionality/otherCases.test.tsx index 115164c0..ad6a4028 100644 --- a/scripts/__tests__/HorizonXText/StoreFunctionality/otherCases.test.js +++ b/scripts/__tests__/HorizonXText/StoreFunctionality/otherCases.test.tsx @@ -1,11 +1,13 @@ +//@ts-ignore import * as Horizon from '@cloudsop/horizon/index.ts'; import { createStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler'; import { triggerClickEvent } from '../../jest/commonComponents'; +import { describe, beforeEach, afterEach, it, expect } from '@jest/globals'; const { unmountComponentAtNode } = Horizon; describe('Self referencing', () => { - let container = null; + let container: HTMLElement | null = null; const BUTTON_ID = 'btn'; const RESULT_ID = 'result'; @@ -15,7 +17,7 @@ describe('Self referencing', () => { val: 2, }, actions: { - magic: function(state) { + increaseVal: function(state) { state.val = state.val * 2 - 1; }, }, @@ -34,7 +36,7 @@ describe('Self referencing', () => { afterEach(() => { unmountComponentAtNode(container); - container.remove(); + container?.remove(); container = null; }); @@ -45,8 +47,8 @@ describe('Self referencing', () => { return (

{store.double}

-
); @@ -54,29 +56,29 @@ describe('Self referencing', () => { Horizon.render(, container); - expect(document.getElementById(RESULT_ID).innerHTML).toBe('4'); + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('4'); Horizon.act(() => { triggerClickEvent(container, BUTTON_ID); }); - expect(document.getElementById(RESULT_ID).innerHTML).toBe('6'); + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('6'); Horizon.act(() => { triggerClickEvent(container, BUTTON_ID); }); - expect(document.getElementById(RESULT_ID).innerHTML).toBe('10'); + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('10'); }); it('should access other stores', () => { const useOtherStore = createStore({ state: {}, actions: { - doMagic: () => useSelfRefStore().magic(), + doIncreaseVal: () => useSelfRefStore().increaseVal(), }, computed: { - magicConstant: () => useSelfRefStore().value, + selfRefStoreValue: () => useSelfRefStore().value, }, }); @@ -85,9 +87,9 @@ describe('Self referencing', () => { return (
-

{store.magicConstant}

-
); @@ -95,13 +97,13 @@ describe('Self referencing', () => { Horizon.render(, container); - expect(document.getElementById(RESULT_ID).innerHTML).toBe('5'); + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('5'); Horizon.act(() => { triggerClickEvent(container, BUTTON_ID); }); - expect(document.getElementById(RESULT_ID).innerHTML).toBe('9'); + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('9'); }); it('should use parametric getters', () => { @@ -138,11 +140,11 @@ describe('Self referencing', () => { } Horizon.render(, container); - expect(document.getElementById(RESULT_ID).innerHTML).toBe('abc'); + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('abc'); Horizon.act(() => { triggerClickEvent(container, BUTTON_ID); }); - expect(document.getElementById(RESULT_ID).innerHTML).toBe('def'); + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('def'); }); }); diff --git a/scripts/__tests__/HorizonXText/StoreFunctionality/reset.test.js b/scripts/__tests__/HorizonXText/StoreFunctionality/reset.js similarity index 65% rename from scripts/__tests__/HorizonXText/StoreFunctionality/reset.test.js rename to scripts/__tests__/HorizonXText/StoreFunctionality/reset.js index f0b349da..0cc4a4d3 100644 --- a/scripts/__tests__/HorizonXText/StoreFunctionality/reset.test.js +++ b/scripts/__tests__/HorizonXText/StoreFunctionality/reset.js @@ -1,14 +1,14 @@ import * as Horizon from '@cloudsop/horizon/index.ts'; -import { createStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler'; -import { triggerClickEvent } from '../../jest/commonComponents'; +import {createStore} from '../../../../libs/horizon/src/horizonx/store/StoreHandler'; +import {triggerClickEvent} from '../../jest/commonComponents'; -const { unmountComponentAtNode } = Horizon; +const {unmountComponentAtNode} = Horizon; describe('Reset', () => { it('RESET NOT IMPLEMENTED', async () => { // console.log('reset functionality is not yet implemented') expect(true).toBe(true); - }); + }) return; let container = null; @@ -19,14 +19,14 @@ describe('Reset', () => { const useCounter = createStore({ state: { - counter: 0, + counter: 0 }, actions: { - increment: function(state) { + increment: function (state) { state.counter++; - }, + } }, - computed: {}, + computed: {} }); beforeEach(() => { @@ -44,25 +44,17 @@ describe('Reset', () => { function App() { const store = useCounter(); - return ( -
-

{store.$state.counter}

- - -
- ); + return
+

{store.$s.counter}

+ + +
} - Horizon.render(, container); + Horizon.render(, container); Horizon.act(() => { triggerClickEvent(container, BUTTON_ID); diff --git a/scripts/__tests__/HorizonXText/StoreFunctionality/store.js b/scripts/__tests__/HorizonXText/StoreFunctionality/store.ts similarity index 100% rename from scripts/__tests__/HorizonXText/StoreFunctionality/store.js rename to scripts/__tests__/HorizonXText/StoreFunctionality/store.ts diff --git a/scripts/__tests__/HorizonXText/adapters/ReduxAdapter.test.js b/scripts/__tests__/HorizonXText/adapters/ReduxAdapter.test.tsx similarity index 59% rename from scripts/__tests__/HorizonXText/adapters/ReduxAdapter.test.js rename to scripts/__tests__/HorizonXText/adapters/ReduxAdapter.test.tsx index 2fa93598..e14c5d9e 100644 --- a/scripts/__tests__/HorizonXText/adapters/ReduxAdapter.test.js +++ b/scripts/__tests__/HorizonXText/adapters/ReduxAdapter.test.tsx @@ -1,9 +1,12 @@ +//@ts-ignore +import * as Horizon from '@cloudsop/horizon/index.ts'; import { createStore, applyMiddleware, combineReducers, - bindActionCreators + bindActionCreators, } from '../../../../libs/horizon/src/horizonx/adapters/redux'; +import { describe, it, expect } from '@jest/globals'; describe('Redux adapter', () => { it('should use getState()', async () => { @@ -12,21 +15,21 @@ describe('Redux adapter', () => { }, 0); expect(reduxStore.getState()).toBe(0); - }) + }); it('Should use default state, dispatch action and update state', async () => { const reduxStore = createStore((state, action) => { switch (action.type) { - case('ADD'): - return {counter: state.counter + 1} + case 'ADD': + return { counter: state.counter + 1 }; default: - return {counter: 0}; + return { counter: 0 }; } }); expect(reduxStore.getState().counter).toBe(0); - reduxStore.dispatch({type: 'ADD'}); + reduxStore.dispatch({ type: 'ADD' }); expect(reduxStore.getState().counter).toBe(1); }); @@ -35,37 +38,37 @@ describe('Redux adapter', () => { let counter = 0; const reduxStore = createStore((state = 0, action) => { switch (action.type) { - case('ADD'): - return state + 1 + case 'ADD': + return state + 1; default: return state; } }); - reduxStore.dispatch({type: 'ADD'}); + reduxStore.dispatch({ type: 'ADD' }); expect(counter).toBe(0); expect(reduxStore.getState()).toBe(1); const unsubscribe = reduxStore.subscribe(() => { counter++; }); - reduxStore.dispatch({type: 'ADD'}); - reduxStore.dispatch({type: 'ADD'}); + reduxStore.dispatch({ type: 'ADD' }); + reduxStore.dispatch({ type: 'ADD' }); expect(counter).toBe(2); expect(reduxStore.getState()).toBe(3); unsubscribe(); - reduxStore.dispatch({type: 'ADD'}); - reduxStore.dispatch({type: 'ADD'}); + reduxStore.dispatch({ type: 'ADD' }); + reduxStore.dispatch({ type: 'ADD' }); expect(counter).toBe(2); expect(reduxStore.getState()).toBe(5); }); it('Should bind action creators', async () => { - const addTodo = (text) => { + const addTodo = text => { return { type: 'ADD_TODO', - text - } - } + text, + }; + }; const reduxStore = createStore((state = [], action) => { if (action.type === 'ADD_TODO') { @@ -74,7 +77,7 @@ describe('Redux adapter', () => { return state; }); - const actions = bindActionCreators({addTodo}, reduxStore.dispatch); + const actions = bindActionCreators({ addTodo }, reduxStore.dispatch); actions.addTodo('todo'); @@ -84,57 +87,57 @@ describe('Redux adapter', () => { it('Should replace reducer', async () => { const reduxStore = createStore((state, action) => { switch (action.type) { - case('ADD'): - return {counter: state.counter + 1} + case 'ADD': + return { counter: state.counter + 1 }; default: - return {counter: 0}; + return { counter: 0 }; } }); - reduxStore.dispatch({type: 'ADD'}); + reduxStore.dispatch({ type: 'ADD' }); expect(reduxStore.getState().counter).toBe(1); reduxStore.replaceReducer((state, action) => { switch (action.type) { - case('SUB'): - return {counter: state.counter - 1} + case 'SUB': + return { counter: state.counter - 1 }; default: - return {counter: 0}; + return { counter: 0 }; } }); - reduxStore.dispatch({type: 'SUB'}); + reduxStore.dispatch({ type: 'SUB' }); expect(reduxStore.getState().counter).toBe(0); - }) + }); it('Should combine reducers', async () => { const booleanReducer = (state = false, action) => { switch (action.type) { - case('TOGGLE'): - return !state - default: - return state; - } - } - - const addReducer = (state = 0, action) => { - switch (action.type) { - case('ADD'): - return state + 1 + case 'TOGGLE': + return !state; default: return state; } }; - const reduxStore = createStore(combineReducers({check: booleanReducer, counter: addReducer})); + const addReducer = (state = 0, action) => { + switch (action.type) { + case 'ADD': + return state + 1; + default: + return state; + } + }; + + const reduxStore = createStore(combineReducers({ check: booleanReducer, counter: addReducer })); expect(reduxStore.getState().counter).toBe(0); expect(reduxStore.getState().check).toBe(false); - reduxStore.dispatch({type: 'ADD'}); - reduxStore.dispatch({type: 'TOGGLE'}); + reduxStore.dispatch({ type: 'ADD' }); + reduxStore.dispatch({ type: 'TOGGLE' }); expect(reduxStore.getState().counter).toBe(1); expect(reduxStore.getState().check).toBe(true); @@ -142,28 +145,32 @@ describe('Redux adapter', () => { it('Should apply enhancers', async () => { let counter = 0; - let middlewareCallList = []; + let middlewareCallList: string[] = []; const callCounter = store => next => action => { middlewareCallList.push('callCounter'); counter++; let result = next(action); return result; - } + }; - const reduxStore = createStore((state, action) => { - switch (action.type) { - case('toggle'): - return { - check: !state.check - } - default: - return state; - } - }, {check: false}, applyMiddleware(callCounter)); + const reduxStore = createStore( + (state, action) => { + switch (action.type) { + case 'toggle': + return { + check: !state.check, + }; + default: + return state; + } + }, + { check: false }, + applyMiddleware(callCounter) + ); - reduxStore.dispatch({type: 'toggle'}); - reduxStore.dispatch({type: 'toggle'}); + reduxStore.dispatch({ type: 'toggle' }); + reduxStore.dispatch({ type: 'toggle' }); expect(counter).toBe(3); // NOTE: first action is always store initialization }); @@ -171,38 +178,42 @@ describe('Redux adapter', () => { it('Should apply multiple enhancers', async () => { let counter = 0; let lastAction = ''; - let middlewareCallList = []; + let middlewareCallList: string[] = []; const callCounter = store => next => action => { middlewareCallList.push('callCounter'); counter++; let result = next(action); return result; - } + }; const lastFunctionStorage = store => next => action => { middlewareCallList.push('lastFunctionStorage'); lastAction = action.type; let result = next(action); return result; - } + }; - const reduxStore = createStore((state, action) => { - switch (action.type) { - case('toggle'): - return { - check: !state.check - } - default: - return state; - } - }, {check: false}, applyMiddleware(callCounter, lastFunctionStorage)); + const reduxStore = createStore( + (state, action) => { + switch (action.type) { + case 'toggle': + return { + check: !state.check, + }; + default: + return state; + } + }, + { check: false }, + applyMiddleware(callCounter, lastFunctionStorage) + ); - reduxStore.dispatch({type: 'toggle'}); + reduxStore.dispatch({ type: 'toggle' }); expect(counter).toBe(2); // NOTE: first action is always store initialization expect(lastAction).toBe('toggle'); - expect(middlewareCallList[0]).toBe("callCounter"); - expect(middlewareCallList[1]).toBe("lastFunctionStorage"); + expect(middlewareCallList[0]).toBe('callCounter'); + expect(middlewareCallList[1]).toBe('lastFunctionStorage'); }); }); diff --git a/scripts/__tests__/HorizonXText/adapters/ReduxAdapterThunk.test.js b/scripts/__tests__/HorizonXText/adapters/ReduxAdapterThunk.test.js deleted file mode 100644 index 3785b92c..00000000 --- a/scripts/__tests__/HorizonXText/adapters/ReduxAdapterThunk.test.js +++ /dev/null @@ -1,34 +0,0 @@ -import { createStore, applyMiddleware, thunk } from '../../../../libs/horizon/src/horizonx/adapters/redux'; - -describe('Redux thunk', () => { - it('should use apply thunk middleware', async () => { - const MAX_TODOS = 5; - - function addTodosIfAllowed(todoText) { - return (dispatch, getState) => { - const state = getState(); - - if (state.todos.length < MAX_TODOS) { - dispatch({ type: 'ADD_TODO', text: todoText }); - } - }; - } - - const todoStore = createStore( - (state = { todos: [] }, action) => { - if (action.type === 'ADD_TODO') { - return { todos: state.todos?.concat(action.text) }; - } - return state; - }, - null, - applyMiddleware(thunk) - ); - - for (let i = 0; i < 10; i++) { - todoStore.dispatch(addTodosIfAllowed('todo no.' + i)); - } - - expect(todoStore.getState().todos.length).toBe(5); - }); -}); diff --git a/scripts/__tests__/HorizonXText/adapters/ReduxAdapterThunk.test.tsx b/scripts/__tests__/HorizonXText/adapters/ReduxAdapterThunk.test.tsx new file mode 100644 index 00000000..39e4fc83 --- /dev/null +++ b/scripts/__tests__/HorizonXText/adapters/ReduxAdapterThunk.test.tsx @@ -0,0 +1,33 @@ +import * as Horizon from '@cloudsop/horizon/index.ts'; +import { createStore, applyMiddleware, thunk } from '../../../../libs/horizon/src/horizonx/adapters/redux'; +import {describe, it, expect} from '@jest/globals'; + +describe('Redux thunk', () => { + it('should use apply thunk middleware', async () => { + const MAX_TODOS = 5; + + function addTodosIfAllowed(todoText) { + return (dispatch, getState) => { + const state = getState(); + + if (state.todos.length < MAX_TODOS) { + dispatch({type: 'ADD_TODO', text: todoText}); + } + } + } + + const todoStore = createStore((state = {todos: []}, action) => { + if (action.type === 'ADD_TODO') { + return {todos: state.todos?.concat(action.text)}; + } + return state; + }, null, applyMiddleware(thunk)); + + for (let i = 0; i < 10; i++) { + //TODO: resolve thunk problems + (todoStore.dispatch as unknown as (delayedAction:(dispatch,getState)=>void)=>void)(addTodosIfAllowed('todo no.' + i)); + } + + expect(todoStore.getState().todos.length).toBe(5); + }); +}); diff --git a/scripts/__tests__/HorizonXText/adapters/ReduxReactAdapter.test.js b/scripts/__tests__/HorizonXText/adapters/ReduxReactAdapter.test.tsx similarity index 50% rename from scripts/__tests__/HorizonXText/adapters/ReduxReactAdapter.test.js rename to scripts/__tests__/HorizonXText/adapters/ReduxReactAdapter.test.tsx index 5977671a..4f6feec6 100644 --- a/scripts/__tests__/HorizonXText/adapters/ReduxReactAdapter.test.js +++ b/scripts/__tests__/HorizonXText/adapters/ReduxReactAdapter.test.tsx @@ -1,3 +1,4 @@ +//@ts-ignore import horizon, * as Horizon from '@cloudsop/horizon/index.ts'; import { batch, @@ -8,17 +9,19 @@ import { useSelector, useStore, createSelectorHook, - createDispatchHook, + createDispatchHook } from '../../../../libs/horizon/src/horizonx/adapters/redux'; -import { triggerClickEvent } from '../../jest/commonComponents'; +import {triggerClickEvent} from '../../jest/commonComponents'; +import {describe, it, beforeEach, afterEach, expect} from '@jest/globals'; +import { ReduxStoreHandler } from '@cloudsop/horizon/src/horizonx/types'; const BUTTON = 'button'; const BUTTON2 = 'button2'; const RESULT = 'result'; -const CONTAINER = 'container'; +const CONTAINER = 'container' -function getE(id) { - return document.getElementById(id); +function getE(id):HTMLElement { + return document.getElementById(id)||document.body; } describe('Redux/React binding adapter', () => { @@ -36,19 +39,17 @@ describe('Redux/React binding adapter', () => { const reduxStore = createStore((state = 'state', action) => state); const Child = () => { - const store = useStore(); + const store = useStore() as unknown as ReduxStoreHandler; return
{store.getState()}
; }; const Wrapper = () => { - return ( - - - - ); + return + + ; }; - Horizon.render(, getE(CONTAINER)); + Horizon.render(, getE(CONTAINER)); expect(getE(RESULT).innerHTML).toBe('state'); }); @@ -60,30 +61,23 @@ describe('Redux/React binding adapter', () => { }); const Child = () => { - const store = useStore(); + const store = useStore() as unknown as ReduxStoreHandler; const dispatch = useDispatch(); - return ( -
-

{store.getState()}

- -
- ); + return
+

{store.getState()}

+ +
; }; const Wrapper = () => { - return ( - - - - ); + return + + ; }; - Horizon.render(, getE(CONTAINER)); + Horizon.render(, getE(CONTAINER)); expect(reduxStore.getState()).toBe(0); @@ -101,32 +95,24 @@ describe('Redux/React binding adapter', () => { }); const Child = () => { - const count = useSelector(state => state); + const count = useSelector((state) => state); const dispatch = useDispatch(); - return ( -
-

{count}

- -
- ); + return
+

{count}

+ +
; }; const Wrapper = () => { - return ( - - - - ); + return + + ; }; - Horizon.render(, getE(CONTAINER)); + Horizon.render(, getE(CONTAINER)); expect(getE(RESULT).innerHTML).toBe('0'); @@ -139,80 +125,58 @@ describe('Redux/React binding adapter', () => { }); it('Should use connect', async () => { - const reduxStore = createStore( - (state, action) => { - switch (action.type) { - case 'INCREMENT': - return { - ...state, - value: state.negative ? state.value - action.amount : state.value + action.amount, - }; - case 'TOGGLE': - return { - ...state, - negative: !state.negative, - }; - default: - return state; - } - }, - { negative: false, value: 0 } - ); + const reduxStore = createStore((state, action) => { + switch (action.type) { + case('INCREMENT'): + return { + ...state, + value: state.negative ? state.value - action.amount : state.value + action.amount + }; + case('TOGGLE'): + return { + ...state, + negative: !state.negative + }; + default: + return state; + } + }, {negative: false, value: 0}); - const Child = connect( - (state, ownProps) => { - // map state to props - return { ...state, ...ownProps }; - }, - (dispatch, ownProps) => { - // map dispatch to props - return { - increment: () => dispatch({ type: 'INCREMENT', amount: ownProps.amount }), - }; - }, - (stateProps, dispatchProps, ownProps) => { - //merge props - return { stateProps, dispatchProps, ownProps }; - }, - {} - )(props => { + const Child = connect((state, ownProps) => { + // map state to props + return {...state, ...ownProps}; + }, (dispatch, ownProps) => { + // map dispatch to props + return { + // @ts-ignore + increment: () => dispatch({type: 'INCREMENT', amount: ownProps?.amount}) + }; + }, (stateProps, dispatchProps, ownProps) => { + //merge props + return {stateProps, dispatchProps, ownProps}; + }, {})((props) => { const n = props.stateProps.negative; - return ( -
-
- {n ? '-' : '+'} - {props.stateProps.value} -
- -
- ); + return
+
{n ? '-' : '+'}{props.stateProps.value}
+ +
; }); const Wrapper = () => { + //@ts-ignore const [amount, setAmount] = Horizon.useState(5); - return ( - - - - - ); + return + + + ; }; - Horizon.render(, getE(CONTAINER)); + Horizon.render(, getE(CONTAINER)); expect(getE(RESULT).innerHTML).toBe('+0'); @@ -231,7 +195,7 @@ describe('Redux/React binding adapter', () => { }); expect(getE(RESULT).innerHTML).toBe('+8'); - }); + }) it('Should batch dispatches', async () => { const reduxStore = createStore((state = 0, action) => { @@ -244,32 +208,22 @@ describe('Redux/React binding adapter', () => { function Counter() { renderCounter++; - const value = useSelector(state => state); + const value = useSelector((state) => state); const dispatch = useDispatch(); - return ( -
-

{value}

- -
- ); + return
+

{value}

+ +
; } - Horizon.render( - - - , - getE(CONTAINER) - ); + Horizon.render(, getE(CONTAINER)); expect(getE(RESULT).innerHTML).toBe('0'); expect(renderCounter).toBe(1); @@ -300,49 +254,33 @@ describe('Redux/React binding adapter', () => { const count = createSelectorHook(counterContext)(); const dispatch = createDispatchHook(counterContext)(); - return ( - - ); + return ; } function Toggle() { const check = createSelectorHook(toggleContext)(); const dispatch = createDispatchHook(toggleContext)(); - return ( - - ); + return ; } function Wrapper() { - return ( -
- - - + return
+ + + - - - -
- ); + + + +
; } - Horizon.render(, getE(CONTAINER)); + Horizon.render(, getE(CONTAINER)); expect(getE(BUTTON).innerHTML).toBe('0'); expect(getE(BUTTON2).innerHTML).toBe('false'); diff --git a/scripts/__tests__/HorizonXText/class/ClassException.test.js b/scripts/__tests__/HorizonXText/class/ClassException.test.tsx similarity index 54% rename from scripts/__tests__/HorizonXText/class/ClassException.test.js rename to scripts/__tests__/HorizonXText/class/ClassException.test.tsx index 64cea5aa..7933d376 100644 --- a/scripts/__tests__/HorizonXText/class/ClassException.test.js +++ b/scripts/__tests__/HorizonXText/class/ClassException.test.tsx @@ -1,14 +1,17 @@ import * as Horizon from '@cloudsop/horizon/index.ts'; -import { clearStore, createStore, useStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler'; -import { Text } from '../../jest/commonComponents'; +import * as LogUtils from '../../jest/logUtils'; +import {clearStore, createStore, useStore} from '../../../../libs/horizon/src/horizonx/store/StoreHandler'; +import {Text, triggerClickEvent} from '../../jest/commonComponents'; +import {getObserver} from '../../../../libs/horizon/src/horizonx/proxy/ProxyHandler'; +import {describe, beforeEach, afterEach, it, expect} from '@jest/globals'; describe('测试 Class VNode 清除时,对引用清除', () => { - const { unmountComponentAtNode } = Horizon; - let container = null; + const {unmountComponentAtNode} = Horizon; + let container:HTMLElement|null = null; let globalState = { name: 'bing dun dun', isWin: true, - isShow: true, + isShow: true }; beforeEach(() => { @@ -23,7 +26,7 @@ describe('测试 Class VNode 清除时,对引用清除', () => { setWin: (state, val) => { state.isWin = val; }, - hide: state => { + hide: (state) => { state.isShow = false; }, updateName: (state, val) => { @@ -36,8 +39,9 @@ describe('测试 Class VNode 清除时,对引用清除', () => { afterEach(() => { // 退出时进行清理 unmountComponentAtNode(container); - container.remove(); + container?.remove(); container = null; + LogUtils.clear(); clearStore('user'); }); @@ -47,23 +51,21 @@ describe('测试 Class VNode 清除时,对引用清除', () => { userStore = useStore('user'); render() { + if(!this.userStore) return
; // Do not modify the store data in the render method. Otherwise, an infinite loop may occur. this.userStore.updateName(this.userStore.name === 'bing dun dun' ? 'huo dun dun' : 'bing dun dun'); - return ( -
- - -
- ); + return
+ + +
; } } expect(() => { - Horizon.render(, container); - }).toThrow( - 'The number of updates exceeds the upper limit 50.\n' + - ' A component maybe repeatedly invokes setState on componentWillUpdate or componentDidUpdate.' - ); + Horizon.render(, container); + }).toThrow('The number of updates exceeds the upper limit 50.\n' + + ' A component maybe repeatedly invokes setState on componentWillUpdate or componentDidUpdate.'); + }); }); diff --git a/scripts/__tests__/HorizonXText/class/ClassStateArray.test.js b/scripts/__tests__/HorizonXText/class/ClassStateArray.test.js deleted file mode 100644 index c61aed9c..00000000 --- a/scripts/__tests__/HorizonXText/class/ClassStateArray.test.js +++ /dev/null @@ -1,220 +0,0 @@ -import * as Horizon from '@cloudsop/horizon/index.ts'; -import { clearStore, createStore, useStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler'; -import { App, Text, triggerClickEvent } from '../../jest/commonComponents'; - -describe('在Class组件中,测试store中的Array', () => { - const { unmountComponentAtNode } = Horizon; - let container = null; - beforeEach(() => { - // 创建一个 DOM 元素作为渲染目标 - container = document.createElement('div'); - document.body.appendChild(container); - - const persons = [ - { name: 'p1', age: 1 }, - { name: 'p2', age: 2 }, - ]; - - createStore({ - id: 'user', - state: { - type: 'bing dun dun', - persons: persons, - }, - actions: { - addOnePerson: (state, person) => { - state.persons.push(person); - }, - delOnePerson: state => { - state.persons.pop(); - }, - clearPersons: state => { - state.persons = null; - }, - }, - }); - }); - - afterEach(() => { - // 退出时进行清理 - unmountComponentAtNode(container); - container.remove(); - container = null; - - clearStore('user'); - }); - - const newPerson = { name: 'p3', age: 3 }; - - class Parent extends Horizon.Component { - userStore = useStore('user'); - - addOnePerson = () => { - this.userStore.addOnePerson(newPerson); - }; - - delOnePerson = () => { - this.userStore.delOnePerson(); - }; - - render() { - return ( -
- - -
{this.props.children}
-
- ); - } - } - - it('测试Array方法: push()、pop()', () => { - class Child extends Horizon.Component { - userStore = useStore('user'); - - render() { - return ( -
- -
- ); - } - } - - Horizon.render(, container); - - expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: 2'); - // 在Array中增加一个对象 - Horizon.act(() => { - triggerClickEvent(container, 'addBtn'); - }); - expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: 3'); - - // 在Array中删除一个对象 - Horizon.act(() => { - triggerClickEvent(container, 'delBtn'); - }); - expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: 2'); - }); - - it('测试Array方法: entries()、push()、shift()、unshift、直接赋值', () => { - let globalStore = null; - - class Child extends Horizon.Component { - userStore = useStore('user'); - - constructor(props) { - super(props); - globalStore = this.userStore; - } - - render() { - const nameList = []; - const entries = this.userStore.$state.persons?.entries(); - if (entries) { - for (const entry of entries) { - nameList.push(entry[1].name); - } - } - - return ( -
- -
- ); - } - } - - Horizon.render(, container); - - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2'); - // push - globalStore.$state.persons.push(newPerson); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3'); - - // shift - globalStore.$state.persons.shift({ name: 'p0', age: 0 }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p2 p3'); - - // 赋值[2] - globalStore.$state.persons[2] = { name: 'p4', age: 4 }; - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p2 p3 p4'); - - // 重新赋值[2] - globalStore.$state.persons[2] = { name: 'p5', age: 5 }; - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p2 p3 p5'); - - // unshift - globalStore.$state.persons.unshift({ name: 'p1', age: 1 }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3 p5'); - - // 重新赋值 null - globalStore.$state.persons = null; - expect(container.querySelector('#nameList').innerHTML).toBe('name list: '); - - // 重新赋值 [{ name: 'p1', age: 1 }] - globalStore.$state.persons = [{ name: 'p1', age: 1 }]; - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1'); - }); - - it('测试Array方法: forEach()', () => { - let globalStore = null; - - class Child extends Horizon.Component { - userStore = useStore('user'); - - constructor(props) { - super(props); - globalStore = this.userStore; - } - - render() { - const nameList = []; - this.userStore.$state.persons?.forEach(per => { - nameList.push(per.name); - }); - - return ( -
- -
- ); - } - } - - Horizon.render(, container); - - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2'); - // push - globalStore.$state.persons.push(newPerson); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3'); - - // shift - globalStore.$state.persons.shift({ name: 'p0', age: 0 }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p2 p3'); - - // 赋值[2] - globalStore.$state.persons[2] = { name: 'p4', age: 4 }; - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p2 p3 p4'); - - // 重新赋值[2] - globalStore.$state.persons[2] = { name: 'p5', age: 5 }; - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p2 p3 p5'); - - // unshift - globalStore.$state.persons.unshift({ name: 'p1', age: 1 }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3 p5'); - - // 重新赋值 null - globalStore.$state.persons = null; - expect(container.querySelector('#nameList').innerHTML).toBe('name list: '); - - // 重新赋值 [{ name: 'p1', age: 1 }] - globalStore.$state.persons = [{ name: 'p1', age: 1 }]; - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1'); - }); -}); diff --git a/scripts/__tests__/HorizonXText/class/ClassStateArray.test.tsx b/scripts/__tests__/HorizonXText/class/ClassStateArray.test.tsx new file mode 100644 index 00000000..2ca08ef5 --- /dev/null +++ b/scripts/__tests__/HorizonXText/class/ClassStateArray.test.tsx @@ -0,0 +1,231 @@ +import * as Horizon from '@cloudsop/horizon/index.ts'; +import * as LogUtils from '../../jest/logUtils'; +import {clearStore, createStore, useStore} from '../../../../libs/horizon/src/horizonx/store/StoreHandler'; +import {App, Text, triggerClickEvent} from '../../jest/commonComponents'; +import {describe, beforeEach, afterEach, it, expect} from '@jest/globals'; + +type Person = {name:string,age:number}; + +const persons:Person[] = [{ name: 'p1', age: 1 }, { name: 'p2', age: 2 }]; +let useUserStore = createStore({ + id: 'user', + state: { + type: 'bing dun dun', + persons: persons, + }, + actions: { + addOnePerson: (state, person) => { + state.persons.push(person); + }, + delOnePerson: (state) => { + state.persons.pop(); + }, + clearPersons: (state) => { + state.persons = []; + }, + }, +}); + +describe('在Class组件中,测试store中的Array', () => { + const { unmountComponentAtNode } = Horizon; + let container:HTMLElement|null = null; + beforeEach(() => { + // 创建一个 DOM 元素作为渲染目标 + container = document.createElement('div'); + document.body.appendChild(container); + }); + + afterEach(() => { + // 退出时进行清理 + unmountComponentAtNode(container); + container?.remove(); + container = null; + LogUtils.clear(); + + clearStore('user'); + }); + + const newPerson = { name: 'p3', age: 3 }; + class Parent extends Horizon.Component { + userStore = useUserStore(); + props:{ + children:any[] + }; + + constructor(props){ + super(props); + this.props = props; + } + + addOnePerson = () => { + this.userStore.addOnePerson(newPerson); + } + + delOnePerson = () => { + this.userStore.delOnePerson(); + } + + render() { + return
+ + +
+ {this.props.children} +
+
+ } + } + + it('测试Array方法: push()、pop()', () => { + class Child extends Horizon.Component { + userStore = useUserStore(); + + render() { + return ( +
+ +
+ ); + } + } + + Horizon.render(, container); + + expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: 2'); + // 在Array中增加一个对象 + Horizon.act(() => { + triggerClickEvent(container, 'addBtn'); + }); + expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: 3'); + + // 在Array中删除一个对象 + Horizon.act(() => { + triggerClickEvent(container, 'delBtn'); + }); + expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: 2'); + }); + + it('测试Array方法: entries()、push()、shift()、unshift、直接赋值', () => { + let globalStore = useUserStore(); + + class Child extends Horizon.Component { + userStore = useUserStore(); + + constructor(props) { + super(props); + globalStore = this.userStore; + } + + render() { + const nameList:string[] = []; + const entries = this.userStore.$s.persons?.entries(); + if (entries) { + for (const entry of entries) { + nameList.push(entry[1].name); + } + } + + return ( +
+ +
+ ); + } + } + + Horizon.render(, container); + + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2'); + // push + globalStore?.$s.persons.push(newPerson); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2 p3'); + + // shift + // @ts-ignore TODO:why has this function argument? + globalStore.$s.persons.shift({ name: 'p0', age: 0 }); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p2 p3'); + + // 赋值[2] + globalStore.$s.persons[2] = { name: 'p4', age: 4 }; + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p2 p3 p4'); + + // 重新赋值[2] + globalStore.$s.persons[2] = { name: 'p5', age: 5 }; + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p2 p3 p5'); + + // unshift + globalStore.$s.persons.unshift({ name: 'p1', age: 1 }); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2 p3 p5'); + + // 重新赋值 [] + globalStore.$s.persons = []; + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: '); + + // 重新赋值 [{ name: 'p1', age: 1 }] + globalStore.$s.persons = [{ name: 'p1', age: 1 }]; + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1'); + }); + + it('测试Array方法: forEach()', () => { + let globalStore = useUserStore(); + globalStore.$s.persons.push({ name: 'p2', age: 2 }); + class Child extends Horizon.Component { + userStore = useUserStore(); + + constructor(props) { + super(props); + globalStore = this.userStore; + } + + render() { + const nameList:string[] = []; + this.userStore.$s.persons.forEach((per:Person) => { + nameList.push(per.name); + }); + + return ( +
+ +
+ ); + } + } + + Horizon.render(, container); + + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2'); + // push + globalStore.$s.persons.push(newPerson); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2 p3'); + + // shift + // @ts-ignore TODO:why has this function argument? + globalStore.$s.persons.shift({ name: 'p0', age: 0 }); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p2 p3'); + + // 赋值[2] + globalStore.$s.persons[2] = { name: 'p4', age: 4 }; + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p2 p3 p4'); + + // 重新赋值[2] + globalStore.$s.persons[2] = { name: 'p5', age: 5 }; + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p2 p3 p5'); + + // unshift + globalStore.$s.persons.unshift({ name: 'p1', age: 1 }); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2 p3 p5'); + + // 重新赋值 [] + globalStore.$s.persons = []; + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: '); + + // 重新赋值 [{ name: 'p1', age: 1 }] + globalStore.$s.persons = [{ name: 'p1', age: 1 }]; + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1'); + }); + +}); diff --git a/scripts/__tests__/HorizonXText/class/ClassStateMap.test.js b/scripts/__tests__/HorizonXText/class/ClassStateMap.test.tsx similarity index 58% rename from scripts/__tests__/HorizonXText/class/ClassStateMap.test.js rename to scripts/__tests__/HorizonXText/class/ClassStateMap.test.tsx index 071ad650..bb790d8a 100644 --- a/scripts/__tests__/HorizonXText/class/ClassStateMap.test.js +++ b/scripts/__tests__/HorizonXText/class/ClassStateMap.test.tsx @@ -1,45 +1,48 @@ import * as Horizon from '@cloudsop/horizon/index.ts'; -import { clearStore, createStore, useStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler'; -import { App, Text, triggerClickEvent } from '../../jest/commonComponents'; +import * as LogUtils from '../../jest/logUtils'; +import {clearStore, createStore, useStore} from '../../../../libs/horizon/src/horizonx/store/StoreHandler'; +import {App, Text, triggerClickEvent} from '../../jest/commonComponents'; +import {describe, beforeEach, afterEach, it, expect} from '@jest/globals'; + +const useUserStore = createStore({ + id: 'user', + state: { + type: 'bing dun dun', + persons: new Map([['p1', 1], ['p2', 2]]), + }, + actions: { + addOnePerson: (state, person) => { + state.persons.set(person.name, person.age); + }, + delOnePerson: (state, person) => { + state.persons.delete(person.name); + }, + clearPersons: (state) => { + state.persons.clear(); + }, + reset: (state)=>{ + state.persons=new Map([['p1', 1], ['p2', 2]]); + } + }, +}); describe('在Class组件中,测试store中的Map', () => { const { unmountComponentAtNode } = Horizon; - let container = null; + let container:HTMLElement|null = null; beforeEach(() => { // 创建一个 DOM 元素作为渲染目标 container = document.createElement('div'); document.body.appendChild(container); - const persons = new Map([ - ['p1', 1], - ['p2', 2], - ]); - - createStore({ - id: 'user', - state: { - type: 'bing dun dun', - persons: persons, - }, - actions: { - addOnePerson: (state, person) => { - state.persons.set(person.name, person.age); - }, - delOnePerson: (state, person) => { - state.persons.delete(person.name); - }, - clearPersons: state => { - state.persons.clear(); - }, - }, - }); + useUserStore().reset(); }); afterEach(() => { // 退出时进行清理 unmountComponentAtNode(container); - container.remove(); + container?.remove(); container = null; + LogUtils.clear(); clearStore('user'); }); @@ -47,17 +50,23 @@ describe('在Class组件中,测试store中的Map', () => { const newPerson = { name: 'p3', age: 3 }; class Parent extends Horizon.Component { - userStore = useStore('user'); + userStore = useUserStore(); + props = {children:[]} + + constructor(props){ + super(props); + this.props = props; + } addOnePerson = () => { this.userStore.addOnePerson(newPerson); - }; + } delOnePerson = () => { this.userStore.delOnePerson(newPerson); - }; + } clearPersons = () => { this.userStore.clearPersons(); - }; + } render() { return ( @@ -71,7 +80,9 @@ describe('在Class组件中,测试store中的Map', () => { -
{this.props.children}
+
+ {this.props.children} +
); } @@ -79,12 +90,12 @@ describe('在Class组件中,测试store中的Map', () => { it('测试Map方法: set()、delete()、clear()', () => { class Child extends Horizon.Component { - userStore = useStore('user'); + userStore = useUserStore(); render() { return (
- +
); } @@ -92,33 +103,33 @@ describe('在Class组件中,测试store中的Map', () => { Horizon.render(, container); - expect(container.querySelector('#size').innerHTML).toBe('persons number: 2'); + expect(container?.querySelector('#size')?.innerHTML).toBe('persons number: 2'); // 在Map中增加一个对象 Horizon.act(() => { triggerClickEvent(container, 'addBtn'); }); - expect(container.querySelector('#size').innerHTML).toBe('persons number: 3'); + expect(container?.querySelector('#size')?.innerHTML).toBe('persons number: 3'); // 在Map中删除一个对象 Horizon.act(() => { triggerClickEvent(container, 'delBtn'); }); - expect(container.querySelector('#size').innerHTML).toBe('persons number: 2'); + expect(container?.querySelector('#size')?.innerHTML).toBe('persons number: 2'); // clear Map Horizon.act(() => { triggerClickEvent(container, 'clearBtn'); }); - expect(container.querySelector('#size').innerHTML).toBe('persons number: 0'); + expect(container?.querySelector('#size')?.innerHTML).toBe('persons number: 0'); }); it('测试Map方法: keys()', () => { class Child extends Horizon.Component { - userStore = useStore('user'); + userStore = useUserStore(); render() { - const nameList = []; - const keys = this.userStore.$state.persons.keys(); + const nameList:string[] = []; + const keys = this.userStore.$s.persons.keys(); for (const key of keys) { nameList.push(key); } @@ -133,33 +144,33 @@ describe('在Class组件中,测试store中的Map', () => { Horizon.render(, container); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2'); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2'); // 在Map中增加一个对象 Horizon.act(() => { triggerClickEvent(container, 'addBtn'); }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3'); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2 p3'); // 在Map中删除一个对象 Horizon.act(() => { triggerClickEvent(container, 'delBtn'); }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2'); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2'); // clear Map Horizon.act(() => { triggerClickEvent(container, 'clearBtn'); }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: '); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: '); }); it('测试Map方法: values()', () => { class Child extends Horizon.Component { - userStore = useStore('user'); + userStore = useUserStore(); render() { - const ageList = []; - const values = this.userStore.$state.persons.values(); + const ageList:number[] = []; + const values = this.userStore.$s.persons.values(); for (const val of values) { ageList.push(val); } @@ -174,33 +185,33 @@ describe('在Class组件中,测试store中的Map', () => { Horizon.render(, container); - expect(container.querySelector('#ageList').innerHTML).toBe('age list: 1 2'); + expect(container?.querySelector('#ageList')?.innerHTML).toBe('age list: 1 2'); // 在Map中增加一个对象 Horizon.act(() => { triggerClickEvent(container, 'addBtn'); }); - expect(container.querySelector('#ageList').innerHTML).toBe('age list: 1 2 3'); + expect(container?.querySelector('#ageList')?.innerHTML).toBe('age list: 1 2 3'); // 在Map中删除一个对象 Horizon.act(() => { triggerClickEvent(container, 'delBtn'); }); - expect(container.querySelector('#ageList').innerHTML).toBe('age list: 1 2'); + expect(container?.querySelector('#ageList')?.innerHTML).toBe('age list: 1 2'); // clear Map Horizon.act(() => { triggerClickEvent(container, 'clearBtn'); }); - expect(container.querySelector('#ageList').innerHTML).toBe('age list: '); + expect(container?.querySelector('#ageList')?.innerHTML).toBe('age list: '); }); it('测试Map方法: entries()', () => { class Child extends Horizon.Component { - userStore = useStore('user'); + userStore = useUserStore(); render() { - const nameList = []; - const entries = this.userStore.$state.persons.entries(); + const nameList:string[] = []; + const entries = this.userStore.$s.persons.entries(); for (const entry of entries) { nameList.push(entry[0]); } @@ -215,33 +226,33 @@ describe('在Class组件中,测试store中的Map', () => { Horizon.render(, container); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2'); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2'); // 在Map中增加一个对象 Horizon.act(() => { triggerClickEvent(container, 'addBtn'); }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3'); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2 p3'); // 在Map中删除一个对象 Horizon.act(() => { triggerClickEvent(container, 'delBtn'); }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2'); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2'); // clear Map Horizon.act(() => { triggerClickEvent(container, 'clearBtn'); }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: '); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: '); }); it('测试Map方法: forEach()', () => { class Child extends Horizon.Component { - userStore = useStore('user'); + userStore = useUserStore(); render() { - const nameList = []; - this.userStore.$state.persons.forEach((val, key) => { + const nameList:string[] = []; + this.userStore.$s.persons.forEach((val, key) => { nameList.push(key); }); @@ -255,34 +266,34 @@ describe('在Class组件中,测试store中的Map', () => { Horizon.render(, container); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2'); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2'); // 在Map中增加一个对象 Horizon.act(() => { triggerClickEvent(container, 'addBtn'); }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3'); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2 p3'); // 在Map中删除一个对象 Horizon.act(() => { triggerClickEvent(container, 'delBtn'); }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2'); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2'); // clear Map Horizon.act(() => { triggerClickEvent(container, 'clearBtn'); }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: '); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: '); }); it('测试Map方法: has()', () => { class Child extends Horizon.Component { - userStore = useStore('user'); + userStore = useUserStore(); render() { return (
- +
); } @@ -290,21 +301,21 @@ describe('在Class组件中,测试store中的Map', () => { Horizon.render(, container); - expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: false'); + expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: false'); // 在Map中增加一个对象 Horizon.act(() => { triggerClickEvent(container, 'addBtn'); }); - expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: true'); + expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: true'); }); it('测试Map方法: for of()', () => { class Child extends Horizon.Component { - userStore = useStore('user'); + userStore = useUserStore(); render() { - const nameList = []; - for (const per of this.userStore.$state.persons) { + const nameList:string[] = []; + for (const per of this.userStore.$s.persons) { nameList.push(per[0]); } @@ -318,23 +329,23 @@ describe('在Class组件中,测试store中的Map', () => { Horizon.render(, container); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2'); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2'); // 在Map中增加一个对象 Horizon.act(() => { triggerClickEvent(container, 'addBtn'); }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3'); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2 p3'); // 在Map中删除一个对象 Horizon.act(() => { triggerClickEvent(container, 'delBtn'); }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2'); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2'); // clear Map Horizon.act(() => { triggerClickEvent(container, 'clearBtn'); }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: '); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: '); }); }); diff --git a/scripts/__tests__/HorizonXText/clear/ClassVNodeClear.test.js b/scripts/__tests__/HorizonXText/clear/ClassVNodeClear.test.tsx similarity index 62% rename from scripts/__tests__/HorizonXText/clear/ClassVNodeClear.test.js rename to scripts/__tests__/HorizonXText/clear/ClassVNodeClear.test.tsx index a2226b1c..4fe4e719 100644 --- a/scripts/__tests__/HorizonXText/clear/ClassVNodeClear.test.js +++ b/scripts/__tests__/HorizonXText/clear/ClassVNodeClear.test.tsx @@ -1,15 +1,17 @@ import * as Horizon from '@cloudsop/horizon/index.ts'; -import { clearStore, createStore, useStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler'; -import { Text, triggerClickEvent } from '../../jest/commonComponents'; -import { getObserver } from '../../../../libs/horizon/src/horizonx/proxy/ProxyHandler'; +import * as LogUtils from '../../jest/logUtils'; +import {clearStore, createStore, useStore} from '../../../../libs/horizon/src/horizonx/store/StoreHandler'; +import {Text, triggerClickEvent} from '../../jest/commonComponents'; +import {getObserver} from '../../../../libs/horizon/src/horizonx/proxy/ProxyHandler'; +import {describe, it, beforeEach, afterEach, expect} from '@jest/globals'; describe('测试 Class VNode 清除时,对引用清除', () => { - const { unmountComponentAtNode } = Horizon; - let container = null; + const {unmountComponentAtNode} = Horizon; + let container:HTMLElement|null = null; let globalState = { name: 'bing dun dun', isWin: true, - isShow: true, + isShow: true }; beforeEach(() => { @@ -24,7 +26,7 @@ describe('测试 Class VNode 清除时,对引用清除', () => { setWin: (state, val) => { state.isWin = val; }, - hide: state => { + hide: (state) => { state.isShow = false; }, updateName: (state, val) => { @@ -37,8 +39,9 @@ describe('测试 Class VNode 清除时,对引用清除', () => { afterEach(() => { // 退出时进行清理 unmountComponentAtNode(container); - container.remove(); + container?.remove(); container = null; + LogUtils.clear(); clearStore('user'); }); @@ -48,14 +51,12 @@ describe('测试 Class VNode 清除时,对引用清除', () => { userStore = useStore('user'); render() { - return ( -
- - {this.userStore.isShow && } -
- ); + return
+ + {this.userStore?.isShow && } +
; } } @@ -63,18 +64,16 @@ describe('测试 Class VNode 清除时,对引用清除', () => { userStore = useStore('user'); setWin = () => { - this.userStore.setWin(!this.userStore.isWin); - }; + this.userStore?.setWin(!this.userStore?.isWin); + } render() { - return ( -
- - {this.userStore.isWin && } -
- ); + return
+ + {this.userStore?.isWin && } +
; } } @@ -84,16 +83,14 @@ describe('测试 Class VNode 清除时,对引用清除', () => { render() { // this.userStore.updateName(this.userStore.name === 'bing dun dun' ? 'huo dun dun' : 'bing dun dun'); - return ( -
- - -
- ); + return
+ + +
; } } - Horizon.render(, container); + Horizon.render(, container); // Parent and Child hold the isWin key expect(getObserver(globalState).keyVNodes.get('isWin').size).toBe(2); diff --git a/scripts/__tests__/HorizonXText/clear/FunctionVNodeClear.test.js b/scripts/__tests__/HorizonXText/clear/FunctionVNodeClear.test.tsx similarity index 59% rename from scripts/__tests__/HorizonXText/clear/FunctionVNodeClear.test.js rename to scripts/__tests__/HorizonXText/clear/FunctionVNodeClear.test.tsx index ead0453a..76633b76 100644 --- a/scripts/__tests__/HorizonXText/clear/FunctionVNodeClear.test.js +++ b/scripts/__tests__/HorizonXText/clear/FunctionVNodeClear.test.tsx @@ -1,15 +1,17 @@ import * as Horizon from '@cloudsop/horizon/index.ts'; -import { clearStore, createStore, useStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler'; -import { Text, triggerClickEvent } from '../../jest/commonComponents'; -import { getObserver } from '../../../../libs/horizon/src/horizonx/proxy/ProxyHandler'; +import * as LogUtils from '../../jest/logUtils'; +import {clearStore, createStore, useStore} from '../../../../libs/horizon/src/horizonx/store/StoreHandler'; +import {Text, triggerClickEvent} from '../../jest/commonComponents'; +import {getObserver} from '../../../../libs/horizon/src/horizonx/proxy/ProxyHandler'; +import {describe, it, beforeEach, afterEach, expect} from '@jest/globals'; describe('测试VNode清除时,对引用清除', () => { - const { unmountComponentAtNode } = Horizon; - let container = null; + const {unmountComponentAtNode} = Horizon; + let container:HTMLElement|null = null; let globalState = { name: 'bing dun dun', isWin: true, - isShow: true, + isShow: true }; beforeEach(() => { @@ -24,9 +26,9 @@ describe('测试VNode清除时,对引用清除', () => { setWin: (state, val) => { state.isWin = val; }, - hide: state => { + hide: (state) => { state.isShow = false; - }, + } }, }); }); @@ -34,8 +36,9 @@ describe('测试VNode清除时,对引用清除', () => { afterEach(() => { // 退出时进行清理 unmountComponentAtNode(container); - container.remove(); + container?.remove(); container = null; + LogUtils.clear(); clearStore('user'); }); @@ -45,14 +48,12 @@ describe('测试VNode清除时,对引用清除', () => { userStore = useStore('user'); render() { - return ( -
- - {this.userStore.isShow && } -
- ); + return
+ + {this.userStore?.isShow && } +
; } } @@ -60,18 +61,16 @@ describe('测试VNode清除时,对引用清除', () => { userStore = useStore('user'); setWin = () => { - this.userStore.setWin(!this.userStore.isWin); - }; + this.userStore?.setWin(!this.userStore.isWin); + } render() { - return ( -
- - {this.userStore.isWin && } -
- ); + return
+ + {this.userStore?.isWin && } +
; } } @@ -79,16 +78,14 @@ describe('测试VNode清除时,对引用清除', () => { userStore = useStore('user'); render() { - return ( -
- - -
- ); + return
+ + +
; } } - Horizon.render(, container); + Horizon.render(, container); // Parent and Child hold the isWin key expect(getObserver(globalState).keyVNodes.get('isWin').size).toBe(2); diff --git a/scripts/__tests__/HorizonXText/edgeCases/proxy.test.js b/scripts/__tests__/HorizonXText/edgeCases/proxy.test.js deleted file mode 100644 index a643f311..00000000 --- a/scripts/__tests__/HorizonXText/edgeCases/proxy.test.js +++ /dev/null @@ -1,21 +0,0 @@ -import { createProxy } from '../../../../libs/horizon/src/horizonx/proxy/ProxyHandler'; - -describe('Proxy', () => { - const arr = []; - - it('Should not double wrap proxies', async () => { - const proxy1 = createProxy(arr); - - const proxy2 = createProxy(proxy1); - - expect(proxy1 === proxy2).toBe(true); - }); - - it('Should re-use existing proxy of same object', async () => { - const proxy1 = createProxy(arr); - - const proxy2 = createProxy(arr); - - expect(proxy1 === proxy2).toBe(true); - }); -}); diff --git a/scripts/__tests__/HorizonXText/edgeCases/proxy.test.tsx b/scripts/__tests__/HorizonXText/edgeCases/proxy.test.tsx new file mode 100644 index 00000000..d402e35c --- /dev/null +++ b/scripts/__tests__/HorizonXText/edgeCases/proxy.test.tsx @@ -0,0 +1,48 @@ +import {createProxy} from '../../../../libs/horizon/src/horizonx/proxy/ProxyHandler'; +import {readonlyProxy} from '../../../../libs/horizon/src/horizonx/proxy/readonlyProxy'; +import {describe, beforeEach, afterEach, it, expect} from '@jest/globals'; + +describe('Proxy', () => { + const arr = []; + + it('Should not double wrap proxies', async () => { + const proxy1 = createProxy(arr); + + const proxy2 = createProxy(proxy1); + + expect(proxy1 === proxy2).toBe(true); + }); + + it('Should re-use existing proxy of same object', async () => { + const proxy1 = createProxy(arr); + + const proxy2 = createProxy(arr); + + expect(proxy1 === proxy2).toBe(true); + }); + + it('Readonly proxy should prevent changes', async () => { + const proxy1 = readonlyProxy([1]); + + try{ + proxy1.push('a'); + expect(true).toBe(false);//we expect exception above + }catch(e){ + //expected + } + + try{ + proxy1[0]=null; + expect(true).toBe(false);//we expect exception above + }catch(e){ + //expected + } + + try{ + delete proxy1[0]; + expect(true).toBe(false);//we expect exception above + }catch(e){ + //expected + } + }); +}); diff --git a/scripts/__tests__/jest/logUtils.js b/scripts/__tests__/jest/logUtils.js new file mode 100644 index 00000000..ecb32c98 --- /dev/null +++ b/scripts/__tests__/jest/logUtils.js @@ -0,0 +1,26 @@ +let dataArray = null; + +const log = value => { + if (dataArray === null) { + dataArray = [value]; + } else { + dataArray.push(value); + } +}; + +const getAndClear = () => { + if (dataArray === null) { + return []; + } + const values = dataArray; + dataArray = null; + return values; +}; + +const clear = () => { + dataArray = dataArray ? null : dataArray; +}; + +exports.clear = clear; +exports.log = log; +exports.getAndClear = getAndClear; diff --git a/scripts/rollup/rollup.config.js b/scripts/rollup/rollup.config.js index c3c31b49..c58fa290 100644 --- a/scripts/rollup/rollup.config.js +++ b/scripts/rollup/rollup.config.js @@ -4,6 +4,7 @@ import path from 'path'; import fs from 'fs'; import replace from '@rollup/plugin-replace'; import copy from './copy-plugin'; +import execute from 'rollup-plugin-execute'; import { terser } from 'rollup-plugin-terser'; import { version as horizonVersion } from '@cloudsop/horizon/package.json'; @@ -58,6 +59,7 @@ function genConfig(mode) { }, preventAssignment: true, }), + execute('npm run build-types'), mode === 'production' && terser(), copy([ { From 8995659ea50434cf60cacf6c12ba0c0439fc8945 Mon Sep 17 00:00:00 2001 From: * <*> Date: Fri, 29 Jul 2022 20:38:43 +0800 Subject: [PATCH 02/38] Match-id-204bc47828a04bfb747ed16f9dbd8d1885dddb7d --- .../src/horizonx/adapters/reduxReact.ts | 11 +++-- .../src/horizonx/store/StoreHandler.ts | 12 ++--- .../StoreFunctionality/basicAccess.test.tsx | 45 +++++++++++++++++++ 3 files changed, 57 insertions(+), 11 deletions(-) diff --git a/libs/horizon/src/horizonx/adapters/reduxReact.ts b/libs/horizon/src/horizonx/adapters/reduxReact.ts index 8e205eb7..0954fbb3 100644 --- a/libs/horizon/src/horizonx/adapters/reduxReact.ts +++ b/libs/horizon/src/horizonx/adapters/reduxReact.ts @@ -33,12 +33,11 @@ export function createSelectorHook(context: Context): (selector?: (any) => any) return function(selector = state => state) { const [b, fr] = useState(false); - const listener = () => { - fr(!b); - }; - useEffect(() => { - return store.subscribe(listener); + const unsubscribe = store.subscribe(() => fr(!b)); + return () => { + unsubscribe(); + }; }); return selector(store.getState()); @@ -110,7 +109,7 @@ export function connect( useEffect(() => { const unsubscribe = store.subscribe(() => forceReload(!f)); - () => { + return () => { unsubscribe(); }; }); diff --git a/libs/horizon/src/horizonx/store/StoreHandler.ts b/libs/horizon/src/horizonx/store/StoreHandler.ts index ffc67aa8..ecbe22d9 100644 --- a/libs/horizon/src/horizonx/store/StoreHandler.ts +++ b/libs/horizon/src/horizonx/store/StoreHandler.ts @@ -15,7 +15,7 @@ function isPromise(obj: any): boolean { type StoreConfig, C extends UserComputedValues> = { state?: S; - options?: { suppressHooks?: boolean }; + // options?: { suppressHooks?: boolean }; actions?: A; id?: string; computed?: C; @@ -27,18 +27,18 @@ export type ReduxStoreHandler = { getState: () => any; subscribe: (listener: () => void) => () => void; replaceReducer: (reducer: (state: any, action: { type: string }) => any) => void; - _horizonXstore: StoreHandler; + // _horizonXstore: StoreHandler; }; type StoreHandler, C extends UserComputedValues> = { $subscribe: (listener: () => void) => void; $unsubscribe: (listener: () => void) => void; $s: S; - $config: StoreConfig; + // $config: StoreConfig; $queue: QueuedStoreActions; $a: StoreActions; $c: UserComputedValues; - reduxHandler?: ReduxStoreHandler; + // reduxHandler?: ReduxStoreHandler; } & { [K in keyof S]: S[K] } & { [K in keyof A]: Action } & { [K in keyof C]: ReturnType }; @@ -256,13 +256,15 @@ function hookStore() { function createStoreHook, C extends UserComputedValues>( storeHandler: StoreHandler ): () => StoreHandler { - return () => { + const storeHook = () => { if (!storeHandler.$config.options?.suppressHooks) { hookStore(); } return storeHandler; }; + + return storeHook; } export function useStore, C extends UserComputedValues>( diff --git a/scripts/__tests__/HorizonXText/StoreFunctionality/basicAccess.test.tsx b/scripts/__tests__/HorizonXText/StoreFunctionality/basicAccess.test.tsx index 0a2c8584..9d95c95f 100644 --- a/scripts/__tests__/HorizonXText/StoreFunctionality/basicAccess.test.tsx +++ b/scripts/__tests__/HorizonXText/StoreFunctionality/basicAccess.test.tsx @@ -107,4 +107,49 @@ describe('Basic store manipulation', () => { expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('5'); }); + + it('should call computed from own actions', () => { + const useIncrementStore = createStore({ + id: 'incrementStore', + state: { + count: 2, + }, + actions: { + doublePlusOne: function(state) { + state.count = this.double + 1; + }, + }, + computed:{ + double: (state) => { + return state.count*2 + } + } + }); + + function App() { + const incrementStore = useIncrementStore(); + + return ( +
+ +

{incrementStore.count}

+
+ ); + } + + Horizon.render(, container); + + Horizon.act(() => { + triggerClickEvent(container, BUTTON_ID); + }); + + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('5'); + }) }); From ccf66f1653c4aee2497db67ca579ab804f1d2593 Mon Sep 17 00:00:00 2001 From: * <*> Date: Mon, 15 Aug 2022 20:47:47 +0800 Subject: [PATCH 03/38] Match-id-5f3ff7a11a13ae24c88d6483333762208df08e28 --- libs/horizon/src/horizonx/adapters/redux.ts | 44 +++- .../adapters/reduxPromiseMiddleware.ts | 0 .../src/horizonx/proxy/ProxyHandler.ts | 4 +- .../proxy/handlers/ObjectProxyHandler.ts | 8 +- .../src/horizonx/store/StoreHandler.ts | 2 +- .../StoreFunctionality/async.test.tsx | 209 ++++++------------ .../adapters/ReduxAdapterPromiseMiddleware.js | 96 -------- 7 files changed, 121 insertions(+), 242 deletions(-) delete mode 100644 libs/horizon/src/horizonx/adapters/reduxPromiseMiddleware.ts delete mode 100644 scripts/__tests__/HorizonXText/adapters/ReduxAdapterPromiseMiddleware.js diff --git a/libs/horizon/src/horizonx/adapters/redux.ts b/libs/horizon/src/horizonx/adapters/redux.ts index fb19cf9a..4372e0fa 100644 --- a/libs/horizon/src/horizonx/adapters/redux.ts +++ b/libs/horizon/src/horizonx/adapters/redux.ts @@ -32,6 +32,43 @@ export type ReduxMiddleware = ( type Reducer = (state: any, action: ReduxAction) => any; +function mergeData(state,data){ + console.log('merging data',{state,data}); + if(!data){ + console.log('!data'); + state.stateWrapper=data; + return; + } + + if(Array.isArray(data) && Array.isArray(state?.stateWrapper)){ + console.log('data is array'); + state.stateWrapper.length = data.length; + data.forEach((item,idx) => { + if(item!=state.stateWrapper[idx]){ + state.stateWrapper[idx]=item; + } + }); + return; + } + + if(typeof data === 'object' && typeof state?.stateWrapper === 'object'){ + console.log('data is object'); + Object.keys(state.stateWrapper).forEach(key => { + if(!data.hasOwnProperty(key)) delete state.stateWrapper[key]; + }) + + Object.entries(data).forEach(([key,value])=>{ + if(state.stateWrapper[key]!==value){ + state.stateWrapper[key]=value; + } + }); + return; + } + + console.log('data is primitive or type mismatch'); + state.stateWrapper = data; +} + export function createStore(reducer: Reducer, preloadedState?: any, enhancers?): ReduxStoreHandler { const store = createStoreX({ id: 'defaultStore', @@ -48,14 +85,19 @@ export function createStore(reducer: Reducer, preloadedState?: any, enhancers?): if (result === undefined) { return; } // NOTE: reducer should never return undefined, in this case, do not change state + // mergeData(state,result); state.stateWrapper = result; }, }, options: { - suppressHooks: true, + reduxAdapter: true, }, })(); + // store.$subscribe(()=>{ + // console.log('changed'); + // }); + const result = { reducer, getState: function() { diff --git a/libs/horizon/src/horizonx/adapters/reduxPromiseMiddleware.ts b/libs/horizon/src/horizonx/adapters/reduxPromiseMiddleware.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/libs/horizon/src/horizonx/proxy/ProxyHandler.ts b/libs/horizon/src/horizonx/proxy/ProxyHandler.ts index a540e199..f67dcc47 100644 --- a/libs/horizon/src/horizonx/proxy/ProxyHandler.ts +++ b/libs/horizon/src/horizonx/proxy/ProxyHandler.ts @@ -38,7 +38,9 @@ export function createProxy(rawObj: any, hookObserver = true): any { // 创建Proxy let proxyObj; - if (isArray(rawObj)) { + if (!hookObserver) { + proxyObj = createObjectProxy(rawObj,true); + } else if (isArray(rawObj)) { // 数组 proxyObj = createArrayProxy(rawObj as []); } else if (isCollection(rawObj)) { diff --git a/libs/horizon/src/horizonx/proxy/handlers/ObjectProxyHandler.ts b/libs/horizon/src/horizonx/proxy/handlers/ObjectProxyHandler.ts index 28749a57..4dc27a0b 100644 --- a/libs/horizon/src/horizonx/proxy/handlers/ObjectProxyHandler.ts +++ b/libs/horizon/src/horizonx/proxy/handlers/ObjectProxyHandler.ts @@ -2,16 +2,16 @@ import { isSame } from '../../CommonUtils'; import { createProxy, getObserver, hookObserverMap } from '../ProxyHandler'; import { OBSERVER_KEY } from '../../Constants'; -export function createObjectProxy(rawObj: T): ProxyHandler { +export function createObjectProxy(rawObj: T, singleLevel = false): ProxyHandler { const proxy = new Proxy(rawObj, { - get, + get: (...args) => get(...args, singleLevel), set, }); return proxy; } -export function get(rawObj: object, key: string | symbol, receiver: any): any { +export function get(rawObj: object, key: string | symbol, receiver: any, singleLevel = false): any { // The observer object of symbol ('_horizonObserver') cannot be accessed from Proxy to prevent errors caused by clonedeep. if (key === OBSERVER_KEY) { return undefined; @@ -34,7 +34,7 @@ export function get(rawObj: object, key: string | symbol, receiver: any): any { // 对于prototype不做代理 if (key !== 'prototype') { // 对于value也需要进一步代理 - const valProxy = createProxy(value, hookObserverMap.get(rawObj)); + const valProxy = singleLevel ? value : createProxy(value, hookObserverMap.get(rawObj)); return valProxy; } diff --git a/libs/horizon/src/horizonx/store/StoreHandler.ts b/libs/horizon/src/horizonx/store/StoreHandler.ts index ecbe22d9..e82a4a96 100644 --- a/libs/horizon/src/horizonx/store/StoreHandler.ts +++ b/libs/horizon/src/horizonx/store/StoreHandler.ts @@ -91,7 +91,7 @@ export function createStore, C extend throw new Error('store obj must be pure object'); } - const proxyObj = createProxy(config.state, !config.options?.suppressHooks); + const proxyObj = createProxy(config.state, !config.options?.reduxAdapter); proxyObj.$pending = false; diff --git a/scripts/__tests__/HorizonXText/StoreFunctionality/async.test.tsx b/scripts/__tests__/HorizonXText/StoreFunctionality/async.test.tsx index 66c287e9..d7cab296 100644 --- a/scripts/__tests__/HorizonXText/StoreFunctionality/async.test.tsx +++ b/scripts/__tests__/HorizonXText/StoreFunctionality/async.test.tsx @@ -1,5 +1,5 @@ //@ts-ignore -import * as Horizon from '@cloudsop/horizon/index.ts'; +import * as Horizon from '../../../../libs/horizon'; import { createStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler'; import { triggerClickEvent } from '../../jest/commonComponents'; import { describe, beforeEach, afterEach, it, expect } from '@jest/globals'; @@ -8,159 +8,90 @@ const { unmountComponentAtNode } = Horizon; function postpone(timer, func) { return new Promise(resolve => { - setTimeout(function() { + window.setTimeout(function () { + console.log('resolving postpone'); resolve(func()); }, timer); }); } -describe('Asynchronous functions', () => { - let container: HTMLElement | null = null; - - const COUNTER_ID = 'counter'; - const TOGGLE_ID = 'toggle'; - const TOGGLE_FAST_ID = 'toggleFast'; - const RESULT_ID = 'result'; - - let useAsyncCounter; +describe('Asynchronous store', () => { + const useAsyncCounter = createStore({ + state: { + counter: 0, + check: false, + }, + actions: { + increment: function (state) { + return new Promise(resolve => { + window.setTimeout(() => { + state.counter++; + resolve(true); + }, 10); + }); + }, + toggle: function (state) { + state.check = !state.check; + }, + reset: function (state) { + state.check = false; + state.counter = 0; + }, + }, + computed: { + value: state => { + return (state.check ? 'true' : 'false') + state.counter; + }, + }, + }); beforeEach(() => { - useAsyncCounter = createStore({ - state: { - counter: 0, - check: false, - }, - actions: { - increment: function(state) { - return new Promise(resolve => { - setTimeout(() => { - state.counter++; - resolve(true); - }, 100); - }); - }, - toggle: function(state) { - state.check = !state.check; - }, - }, - computed: { - value: state => { - return (state.check ? 'true' : 'false') + state.counter; - }, - }, - }); - container = document.createElement('div'); - document.body.appendChild(container); + useAsyncCounter().reset(); }); - afterEach(() => { - unmountComponentAtNode(container); - container?.remove(); - container = null; - }); - - it('Should wait for async actions', async () => { - // @ts-ignore - jest.useRealTimers(); - let globalStore; - - function App() { - const store = useAsyncCounter(); - globalStore = store; - - return ( -
-

{store.value}

- - - -
- ); - } - - Horizon.render(, container); - - // initial state - expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('false0'); - - // slow toggle has nothing to wait for, it is resolved immediately - Horizon.act(() => { - triggerClickEvent(container, TOGGLE_ID); - }); - - expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('true0'); - - // counter increment is slow. slow toggle waits for result - Horizon.act(() => { - triggerClickEvent(container, COUNTER_ID); - }); - Horizon.act(() => { - triggerClickEvent(container, TOGGLE_ID); - }); - - expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('true0'); - - // fast toggle does not wait for counter and it is resolved immediately - Horizon.act(() => { - triggerClickEvent(container, TOGGLE_FAST_ID); - }); - - expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('false0'); - - // at 150ms counter increment will be resolved and slow toggle immediately after - const t150 = postpone(150, () => { - expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('true1'); - }); - - // before that, two more actions are added to queue - another counter and slow toggle - Horizon.act(() => { - triggerClickEvent(container, COUNTER_ID); - }); - Horizon.act(() => { - triggerClickEvent(container, TOGGLE_ID); - }); - - // at 250ms they should be already resolved - const t250 = postpone(250, () => { - expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('false2'); - }); - - await Promise.all([t150, t250]); - }); - - it('call async action by then', async () => { - // @ts-ignore + it('should return promise when queued function is called', () => { jest.useFakeTimers(); - let globalStore; - function App() { - const store = useAsyncCounter(); - globalStore = store; + const store = useAsyncCounter(); - return ( -
-

{store.value}

-
- ); - } + return new Promise(resolve => { + store.$queue.increment().then(() => { + expect(store.counter == 1); + resolve(true); + }); - Horizon.render(, container); - - // call async action by then - globalStore.$queue.increment().then(() => { - expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('false1'); + jest.advanceTimersByTime(150); }); + }); - expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('false0'); + it('should queue async functions', () => { + jest.useFakeTimers(); + return new Promise(resolve => { + const store = useAsyncCounter(); - // past 150 ms - // @ts-ignore - jest.advanceTimersByTime(150); + //initial value + expect(store.value).toBe('false0'); + + // no blocking action action + store.$queue.toggle(); + expect(store.value).toBe('true0'); + + // store is not updated before blocking action is resolved + store.$queue.increment(); + const togglePromise = store.$queue.toggle(); + expect(store.value).toBe('true0'); + + // fast action is resolved immediatelly + store.toggle(); + expect(store.value).toBe('false0'); + + // queued action waits for blocking action to resolve + togglePromise.then(() => { + expect(store.value).toBe('true1'); + resolve(); + }); + + jest.advanceTimersByTime(150); + }); }); }); diff --git a/scripts/__tests__/HorizonXText/adapters/ReduxAdapterPromiseMiddleware.js b/scripts/__tests__/HorizonXText/adapters/ReduxAdapterPromiseMiddleware.js deleted file mode 100644 index 509706cf..00000000 --- a/scripts/__tests__/HorizonXText/adapters/ReduxAdapterPromiseMiddleware.js +++ /dev/null @@ -1,96 +0,0 @@ -export const ActionType = { - Pending: 'PENDING', - Fulfilled: 'FULFILLED', - Rejected: 'REJECTED', -}; - -export const promise = store => next => action => { - //let result = next(action); - store._horizonXstore.$queue.dispatch(action); - return result; -}; - -export function createPromise(config = {}) { - const defaultTypes = [ActionType.Pending, ActionType.Fulfilled, ActionType.Rejected]; - const PROMISE_TYPE_SUFFIXES = config.promiseTypeSuffixes || defaultTypes; - const PROMISE_TYPE_DELIMITER = config.promiseTypeDelimiter || '_'; - - return store => { - const { dispatch } = store; - - return next => action => { - /** - * Instantiate variables to hold: - * (1) the promise - * (2) the data for optimistic updates - */ - let promise; - let data; - - /** - * There are multiple ways to dispatch a promise. The first step is to - * determine if the promise is defined: - * (a) explicitly (action.payload.promise is the promise) - * (b) implicitly (action.payload is the promise) - * (c) as an async function (returns a promise when called) - * - * If the promise is not defined in one of these three ways, we don't do - * anything and move on to the next middleware in the middleware chain. - */ - - // Step 1a: Is there a payload? - if (action.payload) { - const PAYLOAD = action.payload; - - // Step 1.1: Is the promise implicitly defined? - if (isPromise(PAYLOAD)) { - promise = PAYLOAD; - } - - // Step 1.2: Is the promise explicitly defined? - else if (isPromise(PAYLOAD.promise)) { - promise = PAYLOAD.promise; - data = PAYLOAD.data; - } - - // Step 1.3: Is the promise returned by an async function? - else if (typeof PAYLOAD === 'function' || typeof PAYLOAD.promise === 'function') { - promise = PAYLOAD.promise ? PAYLOAD.promise() : PAYLOAD(); - data = PAYLOAD.promise ? PAYLOAD.data : undefined; - - // Step 1.3.1: Is the return of action.payload a promise? - if (!isPromise(promise)) { - // If not, move on to the next middleware. - return next({ - ...action, - payload: promise, - }); - } - } - - // Step 1.4: If there's no promise, move on to the next middleware. - else { - return next(action); - } - - // Step 1b: If there's no payload, move on to the next middleware. - } else { - return next(action); - } - - /** - * Instantiate and define constants for: - * (1) the action type - * (2) the action meta - */ - const TYPE = action.type; - const META = action.meta; - - /** - * Instantiate and define constants for the action type suffixes. - * These are appended to the end of the action type. - */ - const [PENDING, FULFILLED, REJECTED] = PROMISE_TYPE_SUFFIXES; - }; - }; -} From 5d8eebf14487af249fdaab1bbf777799fca2901a Mon Sep 17 00:00:00 2001 From: * <*> Date: Wed, 24 Aug 2022 17:29:09 +0800 Subject: [PATCH 04/38] Match-id-cec2ce18d7d08f5b4f5642c57aaa799038a73223 --- .../src/horizonx/store/StoreHandler.ts | 8 +- .../src/renderer/render/BaseComponent.ts | 3 + .../multipleStores.test.tsx | 186 ++++++++++++++++++ 3 files changed, 192 insertions(+), 5 deletions(-) create mode 100644 scripts/__tests__/HorizonXText/StoreFunctionality/multipleStores.test.tsx diff --git a/libs/horizon/src/horizonx/store/StoreHandler.ts b/libs/horizon/src/horizonx/store/StoreHandler.ts index bdbf8f77..fc3ed6c9 100644 --- a/libs/horizon/src/horizonx/store/StoreHandler.ts +++ b/libs/horizon/src/horizonx/store/StoreHandler.ts @@ -204,7 +204,8 @@ export function createStore, C extend return createStoreHook(handler); } -function clearVNodeObservers(vNode) { +export function clearVNodeObservers(vNode) { + if(!vNode.observers) return; vNode.observers.forEach(observer => { observer.clearByVNode(vNode); }); @@ -220,10 +221,7 @@ function hookStore() { return; } - if (processingVNode.observers) { - // 清除上一次缓存的Observer依赖 - clearVNodeObservers(processingVNode); - } else { + if (!processingVNode.observers) { processingVNode.observers = new Set(); } diff --git a/libs/horizon/src/renderer/render/BaseComponent.ts b/libs/horizon/src/renderer/render/BaseComponent.ts index 1ea097d3..46fd1e0a 100644 --- a/libs/horizon/src/renderer/render/BaseComponent.ts +++ b/libs/horizon/src/renderer/render/BaseComponent.ts @@ -12,6 +12,7 @@ import { FlagUtils } from '../vnode/VNodeFlags'; import {onlyUpdateChildVNodes} from '../vnode/VNodeCreator'; import componentRenders from './index'; import {setProcessingVNode} from '../GlobalVar'; +import { clearVNodeObservers } from '../../horizonx/store/StoreHandler'; // 复用vNode时,也需对stack进行处理 function handlerContext(processing: VNode) { @@ -55,6 +56,8 @@ export function captureVNode(processing: VNode): VNode | null { processing.shouldUpdate = false; setProcessingVNode(processing); + + clearVNodeObservers(processing); const child = component.captureRender(processing, shouldUpdate); setProcessingVNode(null); diff --git a/scripts/__tests__/HorizonXText/StoreFunctionality/multipleStores.test.tsx b/scripts/__tests__/HorizonXText/StoreFunctionality/multipleStores.test.tsx new file mode 100644 index 00000000..a9b933d9 --- /dev/null +++ b/scripts/__tests__/HorizonXText/StoreFunctionality/multipleStores.test.tsx @@ -0,0 +1,186 @@ +//@ts-ignore +import Horizon, { createStore } from '@cloudsop/horizon/index.ts'; +import { triggerClickEvent } from '../../jest/commonComponents'; +import { describe, beforeEach, afterEach, it, expect } from '@jest/globals'; + +const { unmountComponentAtNode } = Horizon; + +const useStore1 = createStore({ + state:{ counter:1 }, + actions:{ + add:(state)=>state.counter++, + reset: (state)=>state.counter=1 + } +}) + +const useStore2 = createStore({ + state:{ counter2:1 }, + actions:{ + add2:(state)=>state.counter2++, + reset: (state)=>state.counter2=1 + } +}) + +describe('Using multiple stores', () => { + let container: HTMLElement | null = null; + + const BUTTON_ID = 'btn'; + const BUTTON_ID2 = 'btn2'; + const RESULT_ID = 'result'; + + beforeEach(() => { + container = document.createElement('div'); + document.body.appendChild(container); + useStore1().reset(); + useStore2().reset(); + }); + + afterEach(() => { + unmountComponentAtNode(container); + container?.remove(); + container = null; + }); + + it('Should use multiple stores in class component', () => { + class App extends Horizon.Component{ + render(){ + const {counter,add} = useStore1(); + const store2 = useStore2(); + const {counter2, add2} = store2; + + return ( +
+ + +

{counter*counter2}

+
+ ) + } + } + + Horizon.render(, container); + + Horizon.act(() => { + triggerClickEvent(container, BUTTON_ID); + triggerClickEvent(container, BUTTON_ID); + triggerClickEvent(container, BUTTON_ID2); + triggerClickEvent(container, BUTTON_ID2); + }); + + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('9'); + }); + + it('Should use use stores in cycles and multiple methods', () => { + interface App { + store:any, + store2:any + } + class App extends Horizon.Component{ + constructor(){ + super(); + this.store = useStore1(); + this.store2 = useStore2() + } + + render(){ + const {counter,add} = useStore1(); + const store2 = useStore2(); + const {counter2, add2} = store2; + + for(let i=0; i<100; i++){ + const {counter,add} = useStore1(); + const store2 = useStore2(); + const {counter2, add2} = store2; + } + + return ( +
+ + +

{counter*counter2}

+
+ ) + } + } + + Horizon.render(, container); + + Horizon.act(() => { + triggerClickEvent(container, BUTTON_ID); + triggerClickEvent(container, BUTTON_ID); + triggerClickEvent(container, BUTTON_ID2); + triggerClickEvent(container, BUTTON_ID2); + }); + + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('9'); + }); + + it('Should use multiple stores in function component', () => { + function App() { + const {counter,add} = useStore1(); + const store2 = useStore2(); + const {counter2, add2} = store2; + + return ( +
+ + +

{counter*counter2}

+
+ ); + } + + Horizon.render(, container); + + Horizon.act(() => { + triggerClickEvent(container, BUTTON_ID); + triggerClickEvent(container, BUTTON_ID); + triggerClickEvent(container, BUTTON_ID2); + triggerClickEvent(container, BUTTON_ID2); + }); + + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('9'); + }); +}); \ No newline at end of file From b20293076caec6184cab54f64d69f18c33bda9da Mon Sep 17 00:00:00 2001 From: * <*> Date: Wed, 31 Aug 2022 16:41:25 +0800 Subject: [PATCH 05/38] Match-id-0c7552eff73b3193eb58fd25a2dfe13e09f93e41 --- libs/horizon/index.ts | 3 + libs/horizon/src/horizonx/proxy/Observer.ts | 2 + .../proxy/handlers/ArrayProxyHandler.ts | 20 ++ .../proxy/handlers/CollectionProxyHandler.ts | 18 ++ .../proxy/handlers/ObjectProxyHandler.ts | 17 ++ .../src/horizonx/store/StoreHandler.ts | 2 +- .../src/renderer/render/BaseComponent.ts | 2 +- .../multipleStores.test.tsx | 186 ------------------ 8 files changed, 62 insertions(+), 188 deletions(-) delete mode 100644 scripts/__tests__/HorizonXText/StoreFunctionality/multipleStores.test.tsx diff --git a/libs/horizon/index.ts b/libs/horizon/index.ts index 9e29e71d..f8035375 100644 --- a/libs/horizon/index.ts +++ b/libs/horizon/index.ts @@ -33,6 +33,7 @@ import { runAsyncEffects } from './src/renderer/submit/HookEffectHandler'; import { createStore, useStore, clearStore } from './src/horizonx/store/StoreHandler'; import * as reduxAdapter from './src/horizonx/adapters/redux'; +import { watch } from './src/horizonx/proxy/watch'; // act用于测试,作用是:如果fun触发了刷新(包含了异步刷新),可以保证在act后面的代码是在刷新完成后才执行。 const act = fun => { @@ -85,6 +86,7 @@ const Horizon = { useStore, clearStore, reduxAdapter, + watch }; export const version = __VERSION__; @@ -125,6 +127,7 @@ export { useStore, clearStore, reduxAdapter, + watch }; export default Horizon; diff --git a/libs/horizon/src/horizonx/proxy/Observer.ts b/libs/horizon/src/horizonx/proxy/Observer.ts index d93c7a05..b00e942e 100644 --- a/libs/horizon/src/horizonx/proxy/Observer.ts +++ b/libs/horizon/src/horizonx/proxy/Observer.ts @@ -33,6 +33,8 @@ export class Observer implements IObserver { listeners:(()=>void)[] = []; + watchers={} as {[key:string]:((key:string, oldValue:any, newValue:any)=>void)[]} + useProp(key: string | symbol): void { const processingVNode = getProcessingVNode(); if (processingVNode === null || !processingVNode.observers) { diff --git a/libs/horizon/src/horizonx/proxy/handlers/ArrayProxyHandler.ts b/libs/horizon/src/horizonx/proxy/handlers/ArrayProxyHandler.ts index a6812bde..dd3fdb0b 100644 --- a/libs/horizon/src/horizonx/proxy/handlers/ArrayProxyHandler.ts +++ b/libs/horizon/src/horizonx/proxy/handlers/ArrayProxyHandler.ts @@ -12,6 +12,20 @@ export function createArrayProxy(rawObj: any[]): any[] { } function get(rawObj: any[], key: string, receiver: any) { + if (key === 'watch'){ + const observer = getObserver(rawObj); + + return (prop:any, handler:(key:string, oldValue:any, newValue:any)=>void)=>{ + if(!observer.watchers[prop]){ + observer.watchers[prop]=[] as ((key:string, oldValue:any, newValue:any)=>void)[]; + } + observer.watchers[prop].push(handler); + return ()=>{ + observer.watchers[prop]=observer.watchers[prop].filter(cb=>cb!==handler); + } + } + } + if (isValidIntegerKey(key) || key === 'length') { return objectGet(rawObj, key, receiver); } @@ -29,6 +43,12 @@ function set(rawObj: any[], key: string, value: any, receiver: any) { const observer = getObserver(rawObj); if (!isSame(newValue, oldValue)) { + if(observer.watchers?.[key]){ + observer.watchers[key].forEach(cb => { + cb(key, oldValue, newValue); + }); + } + observer.setProp(key); } diff --git a/libs/horizon/src/horizonx/proxy/handlers/CollectionProxyHandler.ts b/libs/horizon/src/horizonx/proxy/handlers/CollectionProxyHandler.ts index bd044b44..15aaa682 100644 --- a/libs/horizon/src/horizonx/proxy/handlers/CollectionProxyHandler.ts +++ b/libs/horizon/src/horizonx/proxy/handlers/CollectionProxyHandler.ts @@ -34,6 +34,18 @@ function get(rawObj: { size: number }, key: any, receiver: any): any { } else if (Object.prototype.hasOwnProperty.call(handler, key)) { const value = Reflect.get(handler, key, receiver); return value.bind(null, rawObj); + } else if (key === 'watch'){ + const observer = getObserver(rawObj); + + return (prop:any, handler:(key:string, oldValue:any, newValue:any)=>void)=>{ + if(!observer.watchers[prop]){ + observer.watchers[prop]=[] as ((key:string, oldValue:any, newValue:any)=>void)[]; + } + observer.watchers[prop].push(handler); + return ()=>{ + observer.watchers[prop]=observer.watchers[prop].filter(cb=>cb!==handler); + } + } } return Reflect.get(rawObj, key, receiver); @@ -67,6 +79,12 @@ function set( } if (valChange) { + if(observer.watchers?.[key]){ + observer.watchers[key].forEach(cb => { + cb(key, oldValue, newValue); + }); + } + observer.setProp(key); } diff --git a/libs/horizon/src/horizonx/proxy/handlers/ObjectProxyHandler.ts b/libs/horizon/src/horizonx/proxy/handlers/ObjectProxyHandler.ts index 4dc27a0b..29ab2658 100644 --- a/libs/horizon/src/horizonx/proxy/handlers/ObjectProxyHandler.ts +++ b/libs/horizon/src/horizonx/proxy/handlers/ObjectProxyHandler.ts @@ -19,6 +19,18 @@ export function get(rawObj: object, key: string | symbol, receiver: any, singleL const observer = getObserver(rawObj); + if (key === 'watch'){ + return (prop, handler:(key:string, oldValue:any, newValue:any)=>void)=>{ + if(!observer.watchers[prop]){ + observer.watchers[prop]=[] as ((key:string, oldValue:any, newValue:any)=>void)[]; + } + observer.watchers[prop].push(handler); + return ()=>{ + observer.watchers[prop]=observer.watchers[prop].filter(cb=>cb!==handler); + } + } + } + if (key === 'addListener') { return observer.addListener.bind(observer); } @@ -54,6 +66,11 @@ export function set(rawObj: object, key: string, value: any, receiver: any): boo const ret = Reflect.set(rawObj, key, newValue, receiver); if (!isSame(newValue, oldValue)) { + if(observer.watchers?.[key]){ + observer.watchers[key].forEach(cb => { + cb(key, oldValue, newValue); + }); + } observer.setProp(key); } diff --git a/libs/horizon/src/horizonx/store/StoreHandler.ts b/libs/horizon/src/horizonx/store/StoreHandler.ts index fc3ed6c9..00d4695e 100644 --- a/libs/horizon/src/horizonx/store/StoreHandler.ts +++ b/libs/horizon/src/horizonx/store/StoreHandler.ts @@ -220,7 +220,7 @@ function hookStore() { if (!processingVNode) { return; } - + if (!processingVNode.observers) { processingVNode.observers = new Set(); } diff --git a/libs/horizon/src/renderer/render/BaseComponent.ts b/libs/horizon/src/renderer/render/BaseComponent.ts index 46fd1e0a..4192e100 100644 --- a/libs/horizon/src/renderer/render/BaseComponent.ts +++ b/libs/horizon/src/renderer/render/BaseComponent.ts @@ -57,7 +57,7 @@ export function captureVNode(processing: VNode): VNode | null { setProcessingVNode(processing); - clearVNodeObservers(processing); + if(processing.observers) clearVNodeObservers(processing); const child = component.captureRender(processing, shouldUpdate); setProcessingVNode(null); diff --git a/scripts/__tests__/HorizonXText/StoreFunctionality/multipleStores.test.tsx b/scripts/__tests__/HorizonXText/StoreFunctionality/multipleStores.test.tsx deleted file mode 100644 index a9b933d9..00000000 --- a/scripts/__tests__/HorizonXText/StoreFunctionality/multipleStores.test.tsx +++ /dev/null @@ -1,186 +0,0 @@ -//@ts-ignore -import Horizon, { createStore } from '@cloudsop/horizon/index.ts'; -import { triggerClickEvent } from '../../jest/commonComponents'; -import { describe, beforeEach, afterEach, it, expect } from '@jest/globals'; - -const { unmountComponentAtNode } = Horizon; - -const useStore1 = createStore({ - state:{ counter:1 }, - actions:{ - add:(state)=>state.counter++, - reset: (state)=>state.counter=1 - } -}) - -const useStore2 = createStore({ - state:{ counter2:1 }, - actions:{ - add2:(state)=>state.counter2++, - reset: (state)=>state.counter2=1 - } -}) - -describe('Using multiple stores', () => { - let container: HTMLElement | null = null; - - const BUTTON_ID = 'btn'; - const BUTTON_ID2 = 'btn2'; - const RESULT_ID = 'result'; - - beforeEach(() => { - container = document.createElement('div'); - document.body.appendChild(container); - useStore1().reset(); - useStore2().reset(); - }); - - afterEach(() => { - unmountComponentAtNode(container); - container?.remove(); - container = null; - }); - - it('Should use multiple stores in class component', () => { - class App extends Horizon.Component{ - render(){ - const {counter,add} = useStore1(); - const store2 = useStore2(); - const {counter2, add2} = store2; - - return ( -
- - -

{counter*counter2}

-
- ) - } - } - - Horizon.render(, container); - - Horizon.act(() => { - triggerClickEvent(container, BUTTON_ID); - triggerClickEvent(container, BUTTON_ID); - triggerClickEvent(container, BUTTON_ID2); - triggerClickEvent(container, BUTTON_ID2); - }); - - expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('9'); - }); - - it('Should use use stores in cycles and multiple methods', () => { - interface App { - store:any, - store2:any - } - class App extends Horizon.Component{ - constructor(){ - super(); - this.store = useStore1(); - this.store2 = useStore2() - } - - render(){ - const {counter,add} = useStore1(); - const store2 = useStore2(); - const {counter2, add2} = store2; - - for(let i=0; i<100; i++){ - const {counter,add} = useStore1(); - const store2 = useStore2(); - const {counter2, add2} = store2; - } - - return ( -
- - -

{counter*counter2}

-
- ) - } - } - - Horizon.render(, container); - - Horizon.act(() => { - triggerClickEvent(container, BUTTON_ID); - triggerClickEvent(container, BUTTON_ID); - triggerClickEvent(container, BUTTON_ID2); - triggerClickEvent(container, BUTTON_ID2); - }); - - expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('9'); - }); - - it('Should use multiple stores in function component', () => { - function App() { - const {counter,add} = useStore1(); - const store2 = useStore2(); - const {counter2, add2} = store2; - - return ( -
- - -

{counter*counter2}

-
- ); - } - - Horizon.render(, container); - - Horizon.act(() => { - triggerClickEvent(container, BUTTON_ID); - triggerClickEvent(container, BUTTON_ID); - triggerClickEvent(container, BUTTON_ID2); - triggerClickEvent(container, BUTTON_ID2); - }); - - expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('9'); - }); -}); \ No newline at end of file From fbf7e8370ba029b60a210f68256abde29ca7afbe Mon Sep 17 00:00:00 2001 From: * <*> Date: Mon, 19 Sep 2022 17:19:07 +0800 Subject: [PATCH 06/38] Match-id-0a9b956d94031f8d8f03fb90913325154133063d --- libs/horizon/src/horizonx/adapters/redux.ts | 32 +-- .../src/horizonx/adapters/reduxReact.ts | 77 ++++--- libs/horizon/src/horizonx/proxy/watch.ts | 8 + .../src/horizonx/store/StoreHandler.ts | 21 +- package.json | 5 +- .../StoreFunctionality/basicAccess.test.tsx | 54 +++-- .../StoreFunctionality/watch.test.tsx | 132 ++++++++++++ .../adapters/ReduxReactAdapter.test.tsx | 2 +- .../HorizonXText/adapters/connectTest.tsx | 46 ++++ .../edgeCases/multipleStores.test.tsx | 199 ++++++++++++++++++ 10 files changed, 506 insertions(+), 70 deletions(-) create mode 100644 libs/horizon/src/horizonx/proxy/watch.ts create mode 100644 scripts/__tests__/HorizonXText/StoreFunctionality/watch.test.tsx create mode 100644 scripts/__tests__/HorizonXText/adapters/connectTest.tsx create mode 100644 scripts/__tests__/HorizonXText/edgeCases/multipleStores.test.tsx diff --git a/libs/horizon/src/horizonx/adapters/redux.ts b/libs/horizon/src/horizonx/adapters/redux.ts index 4372e0fa..ed17b5d4 100644 --- a/libs/horizon/src/horizonx/adapters/redux.ts +++ b/libs/horizon/src/horizonx/adapters/redux.ts @@ -32,34 +32,34 @@ export type ReduxMiddleware = ( type Reducer = (state: any, action: ReduxAction) => any; -function mergeData(state,data){ - console.log('merging data',{state,data}); - if(!data){ +function mergeData(state, data) { + console.log('merging data', { state, data }); + if (!data) { console.log('!data'); - state.stateWrapper=data; + state.stateWrapper = data; return; } - if(Array.isArray(data) && Array.isArray(state?.stateWrapper)){ + if (Array.isArray(data) && Array.isArray(state?.stateWrapper)) { console.log('data is array'); state.stateWrapper.length = data.length; - data.forEach((item,idx) => { - if(item!=state.stateWrapper[idx]){ - state.stateWrapper[idx]=item; + data.forEach((item, idx) => { + if (item != state.stateWrapper[idx]) { + state.stateWrapper[idx] = item; } }); return; } - if(typeof data === 'object' && typeof state?.stateWrapper === 'object'){ + if (typeof data === 'object' && typeof state?.stateWrapper === 'object') { console.log('data is object'); Object.keys(state.stateWrapper).forEach(key => { - if(!data.hasOwnProperty(key)) delete state.stateWrapper[key]; - }) + if (!data.hasOwnProperty(key)) delete state.stateWrapper[key]; + }); - Object.entries(data).forEach(([key,value])=>{ - if(state.stateWrapper[key]!==value){ - state.stateWrapper[key]=value; + Object.entries(data).forEach(([key, value]) => { + if (state.stateWrapper[key] !== value) { + state.stateWrapper[key] = value; } }); return; @@ -100,7 +100,7 @@ export function createStore(reducer: Reducer, preloadedState?: any, enhancers?): const result = { reducer, - getState: function() { + getState: function () { return store.$s.stateWrapper; }, subscribe: listener => { @@ -169,7 +169,7 @@ export function bindActionCreators(actionCreators: ActionCreators, dispatch: Dis return boundActionCreators; } -export function compose(middlewares: ReduxMiddleware[]) { +export function compose(...middlewares: ReduxMiddleware[]) { return (store: ReduxStoreHandler, extraArgument: any) => { let val; middlewares.reverse().forEach((middleware: ReduxMiddleware, index) => { diff --git a/libs/horizon/src/horizonx/adapters/reduxReact.ts b/libs/horizon/src/horizonx/adapters/reduxReact.ts index 0954fbb3..09e4b081 100644 --- a/libs/horizon/src/horizonx/adapters/reduxReact.ts +++ b/libs/horizon/src/horizonx/adapters/reduxReact.ts @@ -4,7 +4,8 @@ import { createContext } from '../../renderer/components/context/CreateContext'; import { createElement } from '../../external/JSXElement'; import { BoundActionCreator } from './redux'; import { ReduxAction } from './redux'; -import { ReduxStoreHandler } from '../store/StoreHandler' +import { ReduxStoreHandler } from '../store/StoreHandler'; +import { VNode } from '../../renderer/Types'; const DefaultContext = createContext(null); type Context = typeof DefaultContext; @@ -22,15 +23,15 @@ export function Provider({ return createElement(Context.Provider, { value: store }, children); } -export function createStoreHook(context: Context) { +export function createStoreHook(context: Context): () => ReduxStoreHandler { return () => { - return useContext(context); + return useContext(context) as unknown as ReduxStoreHandler; }; } export function createSelectorHook(context: Context): (selector?: (any) => any) => any { - const store = (createStoreHook(context)() as unknown) as ReduxStoreHandler; - return function(selector = state => state) { + const store = createStoreHook(context)() as unknown as ReduxStoreHandler; + return function (selector = state => state) { const [b, fr] = useState(false); useEffect(() => { @@ -44,9 +45,9 @@ export function createSelectorHook(context: Context): (selector?: (any) => any) }; } -export function createDispatchHook(context: Context): ()=>BoundActionCreator { - const store = (createStoreHook(context)() as unknown) as ReduxStoreHandler; - return function() { +export function createDispatchHook(context: Context): () => BoundActionCreator { + const store = createStoreHook(context)() as unknown as ReduxStoreHandler; + return function () { return action => { store.dispatch(action); }; @@ -84,28 +85,46 @@ export const useStore = () => { // areMergedPropsEqual: shallowCompare // }; -export function connect( - mapStateToProps?: (state: any, ownProps: { [key: string]: any }) => Object, - mapDispatchToProps?: - | { [key: string]: (...args: any[]) => ReduxAction } - | ((dispatch: (action: ReduxAction) => any, ownProps?: Object) => Object), - mergeProps?: (stateProps: Object, dispatchProps: Object, ownProps: Object) => Object, +type MapStateToPropsP = (state: any, ownProps: OwnProps) => StateProps; +type MapDispatchToPropsP = + | { [key: string]: (...args: any[]) => ReduxAction } + | ((dispatch: (action: ReduxAction) => any, ownProps: OwnProps) => DispatchProps); +type MergePropsP = ( + stateProps: StateProps, + dispatchProps: DispatchProps, + ownProps: OwnProps +) => MergedProps; + +type WrappedComponent = (props: OwnProps) => ReturnType; +type OriginalComponent = (props: MergedProps) => ReturnType; +type Connector = (Component: OriginalComponent) => WrappedComponent; + +export function connect( + mapStateToProps: MapStateToPropsP = () => ({} as StateProps), + mapDispatchToProps: MapDispatchToPropsP = () => ({} as DispatchProps), + mergeProps: MergePropsP = ( + stateProps, + dispatchProps, + ownProps + ): MergedProps => ({ ...stateProps, ...dispatchProps, ...ownProps } as MergedProps), options?: { areStatesEqual?: (oldState: any, newState: any) => boolean; - context?: any; // TODO: type this + context?: Context; } -) { +): Connector { if (!options) { options = {}; } - return Component => { + //this component should bear the type returned from mapping functions + return (Component: OriginalComponent): WrappedComponent => { const useStore = createStoreHook(options?.context || DefaultContext); - function Wrapper(props) { + //this component should mimic original type of component used + const Wrapper: WrappedComponent = (props: OwnProps) => { const [f, forceReload] = useState(true); - const store = (useStore() as unknown) as ReduxStoreHandler; + const store = useStore(); useEffect(() => { const unsubscribe = store.subscribe(() => forceReload(!f)); @@ -119,36 +138,34 @@ export function connect( mappedState: {}, }) as { current: { - state: {}; - mappedState: {}; + state: { [key: string]: any }; + mappedState: StateProps; }; }; - let mappedState; + let mappedState: StateProps; if (options?.areStatesEqual) { if (options.areStatesEqual(previous.current.state, store.getState())) { - mappedState = previous.current.mappedState; + mappedState = previous.current.mappedState as StateProps; } else { - mappedState = mapStateToProps ? mapStateToProps(store.getState(), props) : {}; + mappedState = mapStateToProps ? mapStateToProps(store.getState(), props) : ({} as StateProps); previous.current.mappedState = mappedState; } } else { - mappedState = mapStateToProps ? mapStateToProps(store.getState(), props) : {}; + mappedState = mapStateToProps ? mapStateToProps(store.getState(), props) : ({} as StateProps); previous.current.mappedState = mappedState; } - let mappedDispatch: { dispatch?: (action) => void } = {}; + let mappedDispatch: DispatchProps = {} as DispatchProps; if (mapDispatchToProps) { if (typeof mapDispatchToProps === 'object') { Object.entries(mapDispatchToProps).forEach(([key, value]) => { - mappedDispatch[key] = (...args) => { + mappedDispatch[key] = (...args: ReduxAction[]) => { store.dispatch(value(...args)); }; }); } else { mappedDispatch = mapDispatchToProps(store.dispatch, props); } - } else { - mappedDispatch.dispatch = store.dispatch; } const mergedProps = ( mergeProps || @@ -161,7 +178,7 @@ export function connect( const node = createElement(Component, mergedProps); return node; - } + }; return Wrapper; }; diff --git a/libs/horizon/src/horizonx/proxy/watch.ts b/libs/horizon/src/horizonx/proxy/watch.ts new file mode 100644 index 00000000..87f55280 --- /dev/null +++ b/libs/horizon/src/horizonx/proxy/watch.ts @@ -0,0 +1,8 @@ +export function watch(stateVariable:any,listener:(stateVariable:any)=>void){ + listener = listener.bind(null,stateVariable); + stateVariable.addListener(listener); + + return ()=>{ + stateVariable.removeListener(listener); + } +} \ No newline at end of file diff --git a/libs/horizon/src/horizonx/store/StoreHandler.ts b/libs/horizon/src/horizonx/store/StoreHandler.ts index 00d4695e..f8055275 100644 --- a/libs/horizon/src/horizonx/store/StoreHandler.ts +++ b/libs/horizon/src/horizonx/store/StoreHandler.ts @@ -35,9 +35,7 @@ type StoreHandler, C extends UserComp $queue: QueuedStoreActions; $a: StoreActions; $c: UserComputedValues; -} & { [K in keyof S]: S[K] } & - { [K in keyof A]: Action } & - { [K in keyof C]: ReturnType }; +} & { [K in keyof S]: S[K] } & { [K in keyof A]: Action } & { [K in keyof C]: ReturnType }; type PlannedAction> = { action: string; @@ -76,6 +74,7 @@ export function createStore, C extend //create a local shalow copy to ensure consistency (if user would change the config object after store creation) config = { id: config.id, + /* @ts-ignore*/ options: config.options, state: config.state, actions: config.actions ? { ...config.actions } : undefined, @@ -87,6 +86,7 @@ export function createStore, C extend throw new Error('store obj must be pure object'); } + /* @ts-ignore*/ const proxyObj = createProxy(config.state, !config.options?.reduxAdapter); proxyObj.$pending = false; @@ -103,7 +103,7 @@ export function createStore, C extend const $a: Partial> = {}; const $queue: Partial> = {}; const $c: Partial> = {}; - const handler = ({ + const handler = { $subscribe, $unsubscribe, $a: $a as StoreActions, @@ -111,7 +111,7 @@ export function createStore, C extend $c: $c as ComputedValues, $config: config, $queue: $queue as QueuedStoreActions, - } as unknown) as StoreHandler; + } as unknown as StoreHandler; function tryNextAction() { if (!plannedActions.length) { @@ -193,6 +193,9 @@ export function createStore, C extend Object.keys(config.state).forEach(key => { Object.defineProperty(handler, key, { get: () => proxyObj[key], + set: value => { + proxyObj[key] = value; + }, }); }); } @@ -205,7 +208,7 @@ export function createStore, C extend } export function clearVNodeObservers(vNode) { - if(!vNode.observers) return; + if (!vNode.observers) return; vNode.observers.forEach(observer => { observer.clearByVNode(vNode); }); @@ -220,14 +223,14 @@ function hookStore() { if (!processingVNode) { return; } - + if (!processingVNode.observers) { processingVNode.observers = new Set(); } if (processingVNode.tag === FunctionComponent) { // from FunctionComponent - const vNodeRef = (useRef(null) as unknown) as { current: VNode }; + const vNodeRef = useRef(null) as unknown as { current: VNode }; vNodeRef.current = processingVNode; useEffect(() => { @@ -239,7 +242,7 @@ function hookStore() { } else if (processingVNode.tag === ClassComponent) { // from ClassComponent if (!processingVNode.classComponentWillUnmount) { - processingVNode.classComponentWillUnmount = function(vNode) { + processingVNode.classComponentWillUnmount = function (vNode) { clearVNodeObservers(vNode); vNode.observers = null; }; diff --git a/package.json b/package.json index 500c07f5..96429935 100644 --- a/package.json +++ b/package.json @@ -34,11 +34,11 @@ "@babel/plugin-transform-object-super": "7.16.7", "@babel/plugin-transform-parameters": "7.16.7", "@babel/plugin-transform-react-jsx": "7.16.7", + "@babel/plugin-transform-react-jsx-source": "^7.16.7", "@babel/plugin-transform-runtime": "7.16.7", "@babel/plugin-transform-shorthand-properties": "7.16.7", "@babel/plugin-transform-spread": "7.16.7", "@babel/plugin-transform-template-literals": "7.16.7", - "@babel/plugin-transform-react-jsx-source": "^7.16.7", "@babel/preset-env": "7.16.7", "@babel/preset-typescript": "7.16.7", "@rollup/plugin-babel": "^5.3.1", @@ -66,5 +66,8 @@ "engines": { "node": ">=10.x", "npm": ">=7.x" + }, + "dependencies": { + "ejs": "^3.1.8" } } diff --git a/scripts/__tests__/HorizonXText/StoreFunctionality/basicAccess.test.tsx b/scripts/__tests__/HorizonXText/StoreFunctionality/basicAccess.test.tsx index 9d95c95f..9647bb1d 100644 --- a/scripts/__tests__/HorizonXText/StoreFunctionality/basicAccess.test.tsx +++ b/scripts/__tests__/HorizonXText/StoreFunctionality/basicAccess.test.tsx @@ -36,6 +36,34 @@ describe('Basic store manipulation', () => { expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('1'); }); + it('Should use direct setters', () => { + function App() { + const logStore = useLogStore(); + + return ( +
+ +

{logStore.logs[0]}

+
+ ); + } + + Horizon.render(, container); + + Horizon.act(() => { + triggerClickEvent(container, BUTTON_ID); + }); + + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('q'); + }); + it('Should use actions and update components', () => { function App() { const logStore = useLogStore(); @@ -74,7 +102,7 @@ describe('Basic store manipulation', () => { increment: state => { state.count++; }, - doublePlusOne: function(state) { + doublePlusOne: function (state) { state.count = state.count * 2; this.increment(); }, @@ -115,20 +143,20 @@ describe('Basic store manipulation', () => { count: 2, }, actions: { - doublePlusOne: function(state) { + doublePlusOne: function (state) { state.count = this.double + 1; }, }, - computed:{ - double: (state) => { - return state.count*2 - } - } + computed: { + double: state => { + return state.count * 2; + }, + }, }); - + function App() { const incrementStore = useIncrementStore(); - + return (
); } - + Horizon.render(, container); - + Horizon.act(() => { triggerClickEvent(container, BUTTON_ID); }); - + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('5'); - }) + }); }); diff --git a/scripts/__tests__/HorizonXText/StoreFunctionality/watch.test.tsx b/scripts/__tests__/HorizonXText/StoreFunctionality/watch.test.tsx new file mode 100644 index 00000000..806c6029 --- /dev/null +++ b/scripts/__tests__/HorizonXText/StoreFunctionality/watch.test.tsx @@ -0,0 +1,132 @@ +import { createStore } from "@cloudsop/horizon/src/horizonx/store/StoreHandler"; +import { watch } from "@cloudsop/horizon/src/horizonx/proxy/watch"; + +describe("watch",()=>{ + it('shouhld watch promitive state variable', async()=>{ + const useStore = createStore({ + state:{ + variable:'x' + }, + actions:{ + change:(state)=>state.variable = "a" + } + }); + + const store = useStore(); + let counter = 0; + + watch(store.$s,(state)=>{ + counter++; + expect(state.variable).toBe('a'); + }) + + store.change(); + + expect(counter).toBe(1); + }); + it('shouhld watch object variable', async()=>{ + const useStore = createStore({ + state:{ + variable:'x' + }, + actions:{ + change:(state)=>state.variable = "a" + } + }); + + const store = useStore(); + let counter = 0; + + store.$s.watch('variable',()=>{ + counter++; + }) + + store.change(); + + expect(counter).toBe(1); + }); + + it('shouhld watch array item', async()=>{ + const useStore = createStore({ + state:{ + arr:['x'] + }, + actions:{ + change:(state)=>state.arr[0]='a' + } + }); + + const store = useStore(); + let counter = 0; + + store.arr.watch('0',()=>{ + counter++; + }) + + store.change(); + + expect(counter).toBe(1); + }); + + it('shouhld watch collection item', async()=>{ + const useStore = createStore({ + state:{ + collection:new Map([ + ['a', 'a'], + ]) + }, + actions:{ + change:(state)=>state.collection.set('a','x') + } + }); + + const store = useStore(); + let counter = 0; + + store.collection.watch('a',()=>{ + counter++; + }) + + store.change(); + + expect(counter).toBe(1); + }); + + it('should watch multiple variables independedntly', async()=>{ + const useStore = createStore({ + state:{ + bool1:true, + bool2:false + }, + actions:{ + toggle1:state=>state.bool1=!state.bool1, + toggle2:state=>state.bool2=!state.bool2 + } + }); + + let counter1=0; + let counterAll=0; + const store = useStore(); + + watch(store.$s,()=>{ + counterAll++; + }) + + store.$s.watch('bool1',()=>{ + counter1++; + }); + + store.toggle1(); + store.toggle1(); + + store.toggle2(); + + store.toggle1(); + + store.toggle2(); + store.toggle2(); + + expect(counter1).toBe(3); + expect(counterAll).toBe(6); + }) +}) \ No newline at end of file diff --git a/scripts/__tests__/HorizonXText/adapters/ReduxReactAdapter.test.tsx b/scripts/__tests__/HorizonXText/adapters/ReduxReactAdapter.test.tsx index 4f6feec6..12fc9eb9 100644 --- a/scripts/__tests__/HorizonXText/adapters/ReduxReactAdapter.test.tsx +++ b/scripts/__tests__/HorizonXText/adapters/ReduxReactAdapter.test.tsx @@ -293,4 +293,4 @@ describe('Redux/React binding adapter', () => { expect(getE(BUTTON).innerHTML).toBe('1'); expect(getE(BUTTON2).innerHTML).toBe('true'); }); -}); +}); \ No newline at end of file diff --git a/scripts/__tests__/HorizonXText/adapters/connectTest.tsx b/scripts/__tests__/HorizonXText/adapters/connectTest.tsx new file mode 100644 index 00000000..a11b8249 --- /dev/null +++ b/scripts/__tests__/HorizonXText/adapters/connectTest.tsx @@ -0,0 +1,46 @@ +import { createElement } from '../../../../libs/horizon/src/external/JSXElement'; +import { createDomTextVNode } from '../../../../libs/horizon/src/renderer/vnode/VNodeCreator'; +import { createStore } from '../../../../libs/horizon/src/horizonx/adapters/redux'; +import { connect } from '../../../../libs/horizon/src/horizonx/adapters/reduxReact'; + +createStore((state: number = 0, action): number => { + if (action.type === 'add') return state + 1; + return 0; +}); + +type WrappedButtonProps = { add: () => void; count: number; text: string }; + +function Button(props: WrappedButtonProps) { + const { add, count, text } = props; + return createElement( + 'button', + { + onClick: add, + }, + createDomTextVNode(text), + createDomTextVNode(': '), + createDomTextVNode(count) + ); +} + +const connector = connect( + state => ({ count: state }), + dispatch => ({ + add: (): void => { + dispatch({ type: 'add' }); + }, + }), + (stateProps, dispatchProps, ownProps: { text: string }) => ({ + add: dispatchProps.add, + count: stateProps.count, + text: ownProps.text, + }) +); + +const ConnectedButton = connector(Button); + +function App() { + return createElement('div', {}, createElement(ConnectedButton, { text: 'click' })); +} + +export default App; diff --git a/scripts/__tests__/HorizonXText/edgeCases/multipleStores.test.tsx b/scripts/__tests__/HorizonXText/edgeCases/multipleStores.test.tsx new file mode 100644 index 00000000..63fb74e5 --- /dev/null +++ b/scripts/__tests__/HorizonXText/edgeCases/multipleStores.test.tsx @@ -0,0 +1,199 @@ +//@ts-ignore +import Horizon, { createStore } from '@cloudsop/horizon/index.ts'; +import { triggerClickEvent } from '../../jest/commonComponents'; +import { describe, beforeEach, afterEach, it, expect } from '@jest/globals'; + +const { unmountComponentAtNode } = Horizon; + +const useStore1 = createStore({ + state:{ counter:1 }, + actions:{ + add:(state)=>state.counter++, + reset: (state)=>state.counter=1 + } +}) + +const useStore2 = createStore({ + state:{ counter2:1 }, + actions:{ + add2:(state)=>state.counter2++, + reset: (state)=>state.counter2=1 + } +}) + +describe('Using multiple stores', () => { + let container: HTMLElement | null = null; + + const BUTTON_ID = 'btn'; + const BUTTON_ID2 = 'btn2'; + const RESULT_ID = 'result'; + + beforeEach(() => { + container = document.createElement('div'); + document.body.appendChild(container); + useStore1().reset(); + useStore2().reset(); + }); + + afterEach(() => { + unmountComponentAtNode(container); + container?.remove(); + container = null; + }); + + it('Should use multiple stores in class component', () => { + class App extends Horizon.Component{ + render(){ + const {counter,add} = useStore1(); + const {counter2, add2} = useStore2(); + + return ( +
+ + +

{counter} {counter2}

+
+ ) + } + } + + Horizon.render(, container); + + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('1 1'); + Horizon.act(() => { + triggerClickEvent(container, BUTTON_ID); + + }); + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('2 1'); + + Horizon.act(() => { + triggerClickEvent(container, BUTTON_ID2); + + }); + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('2 2'); + + }); + + it('Should use use stores in cycles and multiple methods', () => { + interface App { + store:any, + store2:any + } + class App extends Horizon.Component{ + constructor(){ + super(); + this.store = useStore1(); + this.store2 = useStore2() + } + + render(){ + const {counter,add} = useStore1(); + const store2 = useStore2(); + const {counter2, add2} = store2; + + for(let i=0; i<100; i++){ + const {counter,add} = useStore1(); + const store2 = useStore2(); + const {counter2, add2} = store2; + } + + return ( +
+ + +

{counter} {counter2}

+
+ ) + } + } + + Horizon.render(, container); + + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('1 1'); + Horizon.act(() => { + triggerClickEvent(container, BUTTON_ID); + + }); + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('2 1'); + + Horizon.act(() => { + triggerClickEvent(container, BUTTON_ID2); + + }); + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('2 2'); + + }); + + it('Should use multiple stores in function component', () => { + function App() { + const {counter,add} = useStore1(); + const store2 = useStore2(); + const {counter2, add2} = store2; + + return ( +
+ + +

{counter} {counter2}

+
+ ); + } + + Horizon.render(, container); + + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('1 1'); + Horizon.act(() => { + triggerClickEvent(container, BUTTON_ID); + + }); + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('2 1'); + + Horizon.act(() => { + triggerClickEvent(container, BUTTON_ID2); + + }); + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('2 2'); + }); +}); \ No newline at end of file From 5be06f068356e39e83252ed86c61588839f57066 Mon Sep 17 00:00:00 2001 From: * <*> Date: Sat, 12 Nov 2022 02:03:04 +0800 Subject: [PATCH 07/38] Match-id-efec6baa7c4396c9a4dfb7bb733134ca50f5ff34 --- libs/horizon/src/external/devtools.ts | 2 +- .../src/horizonx/devtools/constants.ts | 8 +++ libs/horizon/src/horizonx/devtools/index.ts | 51 +++++++++++++++++ .../src/horizonx/store/StoreHandler.ts | 57 ++++++++++++++++++- 4 files changed, 116 insertions(+), 2 deletions(-) create mode 100644 libs/horizon/src/horizonx/devtools/constants.ts create mode 100644 libs/horizon/src/horizonx/devtools/index.ts diff --git a/libs/horizon/src/external/devtools.ts b/libs/horizon/src/external/devtools.ts index 1fb1af9b..9e8295cb 100644 --- a/libs/horizon/src/external/devtools.ts +++ b/libs/horizon/src/external/devtools.ts @@ -39,7 +39,7 @@ export const helper = { return { name: HookName.RefHook, hIndex, value: (state as Ref).current }; } else if (isEffectHook(state)) { const name = - state.effectConstant == EffectConstant.LayoutEffect || (EffectConstant.LayoutEffect | EffectConstant.DepsChange) + state.effectConstant == EffectConstant.LayoutEffect || EffectConstant.LayoutEffect | EffectConstant.DepsChange ? HookName.LayoutEffectHook : HookName.EffectHook; return { name, hIndex, value: (state as Effect).effect }; diff --git a/libs/horizon/src/horizonx/devtools/constants.ts b/libs/horizon/src/horizonx/devtools/constants.ts new file mode 100644 index 00000000..df41626f --- /dev/null +++ b/libs/horizon/src/horizonx/devtools/constants.ts @@ -0,0 +1,8 @@ +export const INITIALIZED = 'horizonx store initialized'; +export const STATE_CHANGE = 'horizonx state change'; +export const SUBSCRIBED = 'horizonx subscribed'; +export const UNSUBSCRIBED = 'horizonx unsubscribed'; +export const ACTION = 'horizonx action'; +export const ACTION_QUEUED = 'horizonx action queued'; +export const QUEUE_PENDING = 'horizonx queue pending'; +export const QUEUE_FINISHED = 'horizonx queue finished'; diff --git a/libs/horizon/src/horizonx/devtools/index.ts b/libs/horizon/src/horizonx/devtools/index.ts new file mode 100644 index 00000000..884daabf --- /dev/null +++ b/libs/horizon/src/horizonx/devtools/index.ts @@ -0,0 +1,51 @@ +const sessionId = Date.now(); + +function makeStoreSnapshot({ type, data }) { + const expanded = {}; + Object.keys(data.store.$c).forEach(key => { + expanded[key] = data.store[key]; + }); + data.store.expanded = expanded; + const snapshot = makeProxySnapshot({ + data, + type, + sessionId, + }); + return snapshot; +} + +function makeProxySnapshot(obj) { + let clone; + try { + if (!obj) { + return obj; + } + if (obj.nativeEvent) return obj.type + 'Event'; + if (typeof obj === 'function') { + return obj.toString(); + } + if (Array.isArray(obj)) { + clone = []; + obj.forEach(item => clone.push(makeProxySnapshot(item))); + return clone; + } else if (typeof obj === 'object') { + clone = {}; + Object.entries(obj).forEach(([id, value]) => (clone[id] = makeProxySnapshot(value))); + return clone; + } + return obj; + } catch (err) { + throw console.log('cannot serialize object. ' + err); + } +} + +export const devtools = { + emit: (type, data) => { + console.log('store snapshot:', makeStoreSnapshot({ type, data })); + window.postMessage({ + type: 'HORIZON_DEV_TOOLS', + payload: makeStoreSnapshot({ type, data }), + from: 'dev tool hook', + }); + }, +}; diff --git a/libs/horizon/src/horizonx/store/StoreHandler.ts b/libs/horizon/src/horizonx/store/StoreHandler.ts index f8055275..af44a17f 100644 --- a/libs/horizon/src/horizonx/store/StoreHandler.ts +++ b/libs/horizon/src/horizonx/store/StoreHandler.ts @@ -6,6 +6,23 @@ import readonlyProxy from '../proxy/readonlyProxy'; import { Observer } from '../proxy/Observer'; import { FunctionComponent, ClassComponent } from '../Constants'; import { VNode } from '../../renderer/Types'; +import { devtools } from '../devtools'; +import { + ACTION, + ACTION_QUEUED, + INITIALIZED, + QUEUE_FINISHED, + STATE_CHANGE, + SUBSCRIBED, + UNSUBSCRIBED, +} from '../devtools/constants'; + +const idGenerator = { + id: 0, + get: function (prefix) { + return prefix.toString() + this.id++; + }, +}; const storeMap = new Map>(); @@ -73,7 +90,7 @@ export function createStore, C extend ): () => StoreHandler { //create a local shalow copy to ensure consistency (if user would change the config object after store creation) config = { - id: config.id, + id: config.id || idGenerator.get('UNKNOWN_STORE_'), /* @ts-ignore*/ options: config.options, state: config.state, @@ -92,10 +109,12 @@ export function createStore, C extend proxyObj.$pending = false; const $subscribe = listener => { + devtools.emit(SUBSCRIBED, { store: handler, listener }); proxyObj.addListener(listener); }; const $unsubscribe = listener => { + devtools.emit(UNSUBSCRIBED, handler); proxyObj.removeListener(listener); }; @@ -115,11 +134,13 @@ export function createStore, C extend function tryNextAction() { if (!plannedActions.length) { + devtools.emit(QUEUE_FINISHED, { store: handler }); proxyObj.$pending = false; return; } const nextAction = plannedActions.shift()!; + devtools.emit(ACTION, { store: handler, action: nextAction, fromQueue: true }); const result = config.actions ? config.actions[nextAction.action].bind(handler, proxyObj)(...nextAction.payload) : undefined; @@ -139,6 +160,14 @@ export function createStore, C extend if (config.actions) { Object.keys(config.actions).forEach(action => { ($queue as any)[action] = (...payload) => { + devtools.emit(ACTION_QUEUED, { + store: handler, + action: { + action, + payload, + }, + fromQueue: true, + }); return new Promise(resolve => { if (!proxyObj.$pending) { proxyObj.$pending = true; @@ -164,6 +193,14 @@ export function createStore, C extend }; ($a as any)[action] = function Wrapped(...payload) { + devtools.emit(ACTION, { + store: handler, + action: { + action, + payload, + }, + fromQueue: false, + }); return config.actions![action].bind(handler, proxyObj)(...payload); }; @@ -171,6 +208,14 @@ export function createStore, C extend Object.defineProperty(handler, action, { writable: false, value: (...payload) => { + devtools.emit(ACTION, { + store: handler, + action: { + action, + payload, + }, + fromQueue: false, + }); return config.actions![action].bind(handler, proxyObj)(...payload); }, }); @@ -204,6 +249,16 @@ export function createStore, C extend storeMap.set(config.id, handler); } + devtools.emit(INITIALIZED, { + store: handler, + }); + + handler.$subscribe(() => { + devtools.emit(STATE_CHANGE, { + store: handler, + }); + }); + return createStoreHook(handler); } From c0b8a5d03ae69c58b4e7805f956b0107c58e28b0 Mon Sep 17 00:00:00 2001 From: * <*> Date: Mon, 14 Nov 2022 18:07:24 +0800 Subject: [PATCH 08/38] Match-id-71b6384a6cb67e909c9c996382413fe86a027a06 --- libs/horizon/src/horizonx/proxy/ProxyHandler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/horizon/src/horizonx/proxy/ProxyHandler.ts b/libs/horizon/src/horizonx/proxy/ProxyHandler.ts index 9ea2c7af..ccc54ab1 100644 --- a/libs/horizon/src/horizonx/proxy/ProxyHandler.ts +++ b/libs/horizon/src/horizonx/proxy/ProxyHandler.ts @@ -29,7 +29,7 @@ export const hookObserverMap = new WeakMap(); export function createProxy(rawObj: any, isHookObserver = true): any { // 不是对象(是原始数据类型)不用代理 - if (!isObject(rawObj)) { + if (!(rawObj && isObject(rawObj))) { return rawObj; } From 3bd5d456216fe72c2506258431daf9fb26b51fa5 Mon Sep 17 00:00:00 2001 From: * <*> Date: Mon, 14 Nov 2022 18:37:15 +0800 Subject: [PATCH 09/38] Match-id-1f56d48f42d368d727c6a47fd2d56ee9b3603fc6 --- libs/horizon/src/horizonx/proxy/handlers/ObjectProxyHandler.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libs/horizon/src/horizonx/proxy/handlers/ObjectProxyHandler.ts b/libs/horizon/src/horizonx/proxy/handlers/ObjectProxyHandler.ts index a231988e..18ce8f1b 100644 --- a/libs/horizon/src/horizonx/proxy/handlers/ObjectProxyHandler.ts +++ b/libs/horizon/src/horizonx/proxy/handlers/ObjectProxyHandler.ts @@ -70,6 +70,7 @@ export function get(rawObj: object, key: string | symbol, receiver: any, singleL } export function set(rawObj: object, key: string, value: any, receiver: any): boolean { + const oldObject = JSON.stringify(rawObj); const observer = getObserver(rawObj); if (value && key == 'removeListener') { @@ -89,5 +90,6 @@ export function set(rawObj: object, key: string, value: any, receiver: any): boo observer.setProp(key); } + console.log('mutation from: ', JSON.parse(oldObject), ' to: ', ret); return ret; } From ef3e42adbe1844516a7ad50ea8dabc938ca6b4ac Mon Sep 17 00:00:00 2001 From: * <*> Date: Mon, 14 Nov 2022 18:45:51 +0800 Subject: [PATCH 10/38] Match-id-926c1d67d360579f81de7d630a2785652b57aabe --- libs/horizon/src/horizonx/proxy/handlers/ObjectProxyHandler.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/libs/horizon/src/horizonx/proxy/handlers/ObjectProxyHandler.ts b/libs/horizon/src/horizonx/proxy/handlers/ObjectProxyHandler.ts index 18ce8f1b..b2eba435 100644 --- a/libs/horizon/src/horizonx/proxy/handlers/ObjectProxyHandler.ts +++ b/libs/horizon/src/horizonx/proxy/handlers/ObjectProxyHandler.ts @@ -70,6 +70,7 @@ export function get(rawObj: object, key: string | symbol, receiver: any, singleL } export function set(rawObj: object, key: string, value: any, receiver: any): boolean { + console.log('ObjectProxyHandler.set()'); const oldObject = JSON.stringify(rawObj); const observer = getObserver(rawObj); From f37a70552a1020aae89a496d3a5545bb4cf415e8 Mon Sep 17 00:00:00 2001 From: * <*> Date: Fri, 18 Nov 2022 22:13:06 +0800 Subject: [PATCH 11/38] Match-id-a610f3bb353c9901f46e8f676e3d6b2092a862b6 --- libs/horizon/src/horizonx/CommonUtils.ts | 91 ++++++++++++++++++- libs/horizon/src/horizonx/devtools/index.ts | 1 + .../src/horizonx/proxy/HooklessObserver.ts | 14 +-- libs/horizon/src/horizonx/proxy/Observer.ts | 22 ++--- .../proxy/handlers/ArrayProxyHandler.ts | 9 +- .../proxy/handlers/CollectionProxyHandler.ts | 17 ++-- .../proxy/handlers/ObjectProxyHandler.ts | 11 +-- .../src/horizonx/store/StoreHandler.ts | 3 +- libs/horizon/src/horizonx/types.d.ts | 26 +++--- .../StoreFunctionality/utils.test.js | 80 ++++++++++++++++ 10 files changed, 221 insertions(+), 53 deletions(-) create mode 100644 scripts/__tests__/HorizonXText/StoreFunctionality/utils.test.js diff --git a/libs/horizon/src/horizonx/CommonUtils.ts b/libs/horizon/src/horizonx/CommonUtils.ts index e799c963..e6387b6a 100644 --- a/libs/horizon/src/horizonx/CommonUtils.ts +++ b/libs/horizon/src/horizonx/CommonUtils.ts @@ -19,19 +19,31 @@ export function isObject(obj: any): boolean { } export function isSet(obj: any): boolean { - return (obj !== null || obj !== undefined) && (Object.prototype.toString.call(obj) === '[object Set]' || obj.constructor === Set); + return ( + (obj !== null || obj !== undefined) && + (Object.prototype.toString.call(obj) === '[object Set]' || obj.constructor === Set) + ); } export function isWeakSet(obj: any): boolean { - return (obj !== null || obj !== undefined) && (Object.prototype.toString.call(obj) === '[object WeakSet]' || obj.constructor === WeakSet); + return ( + (obj !== null || obj !== undefined) && + (Object.prototype.toString.call(obj) === '[object WeakSet]' || obj.constructor === WeakSet) + ); } export function isMap(obj: any): boolean { - return (obj !== null || obj !== undefined) && (Object.prototype.toString.call(obj) === '[object Map]' || obj.constructor === Map); + return ( + (obj !== null || obj !== undefined) && + (Object.prototype.toString.call(obj) === '[object Map]' || obj.constructor === Map) + ); } export function isWeakMap(obj: any): boolean { - return (obj !== null || obj !== undefined) && (Object.prototype.toString.call(obj) === '[object WeakMap]' || obj.constructor === WeakMap); + return ( + (obj !== null || obj !== undefined) && + (Object.prototype.toString.call(obj) === '[object WeakMap]' || obj.constructor === WeakMap) + ); } export function isArray(obj: any): boolean { @@ -68,3 +80,74 @@ export function isSame(x, y) { return Object.is(x, y); } } + +export function getDetailedType(val: any) { + if (val === undefined) return 'undefined'; + if (val === null) return 'null'; + if (isCollection(val)) return 'collection'; + if (isPromise(val)) return 'promise'; + if (isArray(val)) return 'array'; + if (isWeakMap(val)) return 'weakMap'; + if (isMap(val)) return 'map'; + if (isWeakSet(val)) return 'weakSet'; + if (isSet(val)) return 'set'; + return typeof val; +} + +export function resolveMutation(from, to) { + if (getDetailedType(from) !== getDetailedType(to)) { + return { mutation: true, from, to }; + } + + switch (getDetailedType(from)) { + case 'array': { + let len = Math.max(from.length, to.length); + const res: any[] = []; + let found = false; + for (let i = 0; i < len; i++) { + if (from.length <= i) { + res[i] = { mutation: true, to: to[i] }; + found = true; + } else if (to.length <= i) { + res[i] = { mutation: true, from: from[i] }; + found = true; + } else { + res[i] = resolveMutation(from[i], to[i]); + if (res[i].mutation) found = true; + } + } + // TODO: resolve shifts + return { mutation: found, items: res, from, to }; + } + + case 'object': { + let keys = Object.keys({ ...from, ...to }); + const res = {}; + let found = false; + keys.forEach(key => { + if (!(key in from)) { + res[key] = { mutation: true, to: to[key] }; + found = true; + return; + } + + if (!(key in to)) { + res[key] = { mutation: true, from: from[key] }; + found = true; + return; + } + res[key] = resolveMutation(from[key], to[key]); + if (res[key].mutation) found = true; + }); + return { mutation: found, attributes: res, from, to }; + } + + // TODO: implement collections + + default: { + if (from === to) return { mutation: false }; + + return { mutation: true, from, to }; + } + } +} diff --git a/libs/horizon/src/horizonx/devtools/index.ts b/libs/horizon/src/horizonx/devtools/index.ts index 884daabf..5aecbff3 100644 --- a/libs/horizon/src/horizonx/devtools/index.ts +++ b/libs/horizon/src/horizonx/devtools/index.ts @@ -41,6 +41,7 @@ function makeProxySnapshot(obj) { export const devtools = { emit: (type, data) => { + if (!window['__HORIZON_DEV_HOOK__']) return; console.log('store snapshot:', makeStoreSnapshot({ type, data })); window.postMessage({ type: 'HORIZON_DEV_TOOLS', diff --git a/libs/horizon/src/horizonx/proxy/HooklessObserver.ts b/libs/horizon/src/horizonx/proxy/HooklessObserver.ts index ba390a3a..dd5015e9 100644 --- a/libs/horizon/src/horizonx/proxy/HooklessObserver.ts +++ b/libs/horizon/src/horizonx/proxy/HooklessObserver.ts @@ -19,28 +19,28 @@ import type { IObserver } from './Observer'; * 一个对象(对象、数组、集合)对应一个Observer */ export class HooklessObserver implements IObserver { - listeners: (() => void)[] = []; + listeners: ((mutation) => void)[] = []; useProp(key: string | symbol): void {} - addListener(listener: () => void) { + addListener(listener: (mutation) => void) { this.listeners.push(listener); } - removeListener(listener: () => void) { + removeListener(listener: (mutation) => void) { this.listeners = this.listeners.filter(item => item != listener); } - setProp(key: string | symbol): void { - this.triggerChangeListeners(); + setProp(key: string | symbol, mutation: any): void { + this.triggerChangeListeners(mutation); } - triggerChangeListeners(): void { + triggerChangeListeners(mutation: any): void { this.listeners.forEach(listener => { if (!listener) { return; } - listener(); + listener(mutation); }); } diff --git a/libs/horizon/src/horizonx/proxy/Observer.ts b/libs/horizon/src/horizonx/proxy/Observer.ts index 0db792a0..2f3b15f8 100644 --- a/libs/horizon/src/horizonx/proxy/Observer.ts +++ b/libs/horizon/src/horizonx/proxy/Observer.ts @@ -24,9 +24,9 @@ export interface IObserver { removeListener: (listener: () => void) => void; - setProp: (key: string) => void; + setProp: (key: string, mutation: any) => void; - triggerChangeListeners: () => void; + triggerChangeListeners: (mutation: any) => void; triggerUpdate: (vNode: any) => void; @@ -43,9 +43,9 @@ export class Observer implements IObserver { keyVNodes = new Map(); - listeners: (() => void)[] = []; + listeners: ((mutation) => void)[] = []; - watchers = {} as { [key: string]: ((key: string, oldValue: any, newValue: any) => void)[] }; + watchers = {} as { [key: string]: ((key: string, oldValue: any, newValue: any, mutation: any) => void)[] }; // 对象的属性被使用时调用 useProp(key: string | symbol): void { @@ -76,7 +76,7 @@ export class Observer implements IObserver { } // 对象的属性被赋值时调用 - setProp(key: string | symbol): void { + setProp(key: string | symbol, mutation: any): void { const vNodes = this.keyVNodes.get(key); vNodes?.forEach((vNode: VNode) => { if (vNode.isStoreChange) { @@ -89,7 +89,7 @@ export class Observer implements IObserver { this.triggerUpdate(vNode); }); - this.triggerChangeListeners(); + this.triggerChangeListeners(mutation); } triggerUpdate(vNode: VNode): void { @@ -101,16 +101,16 @@ export class Observer implements IObserver { launchUpdateFromVNode(vNode); } - addListener(listener: () => void): void { + addListener(listener: (mutation) => void): void { this.listeners.push(listener); } - removeListener(listener: () => void): void { + removeListener(listener: (mutation) => void): void { this.listeners = this.listeners.filter(item => item != listener); } - triggerChangeListeners(): void { - this.listeners.forEach(listener => listener()); + triggerChangeListeners(mutation: any): void { + this.listeners.forEach(listener => listener(mutation)); } // 触发所有使用的props的VNode更新 @@ -118,7 +118,7 @@ export class Observer implements IObserver { const keyIt = this.keyVNodes.keys(); let keyItem = keyIt.next(); while (!keyItem.done) { - this.setProp(keyItem.value); + this.setProp(keyItem.value, {}); keyItem = keyIt.next(); } } diff --git a/libs/horizon/src/horizonx/proxy/handlers/ArrayProxyHandler.ts b/libs/horizon/src/horizonx/proxy/handlers/ArrayProxyHandler.ts index 8279512f..672253f8 100644 --- a/libs/horizon/src/horizonx/proxy/handlers/ArrayProxyHandler.ts +++ b/libs/horizon/src/horizonx/proxy/handlers/ArrayProxyHandler.ts @@ -16,6 +16,7 @@ import { getObserver } from '../ProxyHandler'; import { isSame, isValidIntegerKey } from '../../CommonUtils'; import { get as objectGet } from './ObjectProxyHandler'; +import { resolveMutation } from '../../CommonUtils'; export function createArrayProxy(rawObj: any[]): any[] { const handle = { @@ -53,6 +54,8 @@ function set(rawObj: any[], key: string, value: any, receiver: any) { const oldLength = rawObj.length; const newValue = value; + const oldArray = JSON.parse(JSON.stringify(rawObj)); + const ret = Reflect.set(rawObj, key, newValue, receiver); const newLength = rawObj.length; @@ -62,17 +65,17 @@ function set(rawObj: any[], key: string, value: any, receiver: any) { // 值不一样,触发监听器 if (observer.watchers?.[key]) { observer.watchers[key].forEach(cb => { - cb(key, oldValue, newValue); + cb(key, oldValue, newValue, resolveMutation(oldArray, rawObj)); }); } // 触发属性变化 - observer.setProp(key); + observer.setProp(key, resolveMutation(oldValue, rawObj)); } if (oldLength !== newLength) { // 触发数组的大小变化 - observer.setProp('length'); + observer.setProp('length', resolveMutation(oldValue, rawObj)); } return ret; diff --git a/libs/horizon/src/horizonx/proxy/handlers/CollectionProxyHandler.ts b/libs/horizon/src/horizonx/proxy/handlers/CollectionProxyHandler.ts index dad28366..74fa57bc 100644 --- a/libs/horizon/src/horizonx/proxy/handlers/CollectionProxyHandler.ts +++ b/libs/horizon/src/horizonx/proxy/handlers/CollectionProxyHandler.ts @@ -15,6 +15,7 @@ import { createProxy, getObserver, hookObserverMap } from '../ProxyHandler'; import { isMap, isWeakMap, isSame } from '../../CommonUtils'; +import { resolveMutation } from '../../CommonUtils'; const COLLECTION_CHANGE = '_collectionChange'; const handler = { @@ -91,17 +92,17 @@ function set( const observer = getObserver(rawObj); if (valChange || !rawObj.has(key)) { - observer.setProp(COLLECTION_CHANGE); + observer.setProp(COLLECTION_CHANGE, resolveMutation(oldValue, rawObj)); } if (valChange) { if (observer.watchers?.[key]) { observer.watchers[key].forEach(cb => { - cb(key, oldValue, newValue); + cb(key, oldValue, newValue, resolveMutation(oldValue, rawObj)); }); } - observer.setProp(key); + observer.setProp(key, resolveMutation(oldValue, rawObj)); } return rawObj; @@ -109,12 +110,13 @@ function set( // Set的add方法 function add(rawObj: { add: (any) => void; set: (string, any) => any; has: (any) => boolean }, value: any): Object { + const oldCollection = JSON.parse(JSON.stringify(rawObj)); if (!rawObj.has(value)) { rawObj.add(value); const observer = getObserver(rawObj); - observer.setProp(value); - observer.setProp(COLLECTION_CHANGE); + observer.setProp(value, resolveMutation(oldCollection, rawObj)); + observer.setProp(COLLECTION_CHANGE, resolveMutation(oldCollection, rawObj)); } return rawObj; @@ -138,12 +140,13 @@ function clear(rawObj: { size: number; clear: () => void }) { } function deleteFun(rawObj: { has: (key: any) => boolean; delete: (key: any) => void }, key: any) { + const oldCollection = JSON.parse(JSON.stringify(rawObj)); if (rawObj.has(key)) { rawObj.delete(key); const observer = getObserver(rawObj); - observer.setProp(key); - observer.setProp(COLLECTION_CHANGE); + observer.setProp(key, resolveMutation(oldCollection, rawObj)); + observer.setProp(COLLECTION_CHANGE, resolveMutation(oldCollection, rawObj)); return true; } diff --git a/libs/horizon/src/horizonx/proxy/handlers/ObjectProxyHandler.ts b/libs/horizon/src/horizonx/proxy/handlers/ObjectProxyHandler.ts index b2eba435..9e0aac15 100644 --- a/libs/horizon/src/horizonx/proxy/handlers/ObjectProxyHandler.ts +++ b/libs/horizon/src/horizonx/proxy/handlers/ObjectProxyHandler.ts @@ -13,7 +13,7 @@ * See the Mulan PSL v2 for more details. */ -import { isSame } from '../../CommonUtils'; +import { isSame, resolveMutation } from '../../CommonUtils'; import { createProxy, getObserver, hookObserverMap } from '../ProxyHandler'; import { OBSERVER_KEY } from '../../Constants'; @@ -70,8 +70,7 @@ export function get(rawObj: object, key: string | symbol, receiver: any, singleL } export function set(rawObj: object, key: string, value: any, receiver: any): boolean { - console.log('ObjectProxyHandler.set()'); - const oldObject = JSON.stringify(rawObj); + const oldObject = JSON.parse(JSON.stringify(rawObj)); const observer = getObserver(rawObj); if (value && key == 'removeListener') { @@ -85,12 +84,10 @@ export function set(rawObj: object, key: string, value: any, receiver: any): boo if (!isSame(newValue, oldValue)) { if (observer.watchers?.[key]) { observer.watchers[key].forEach(cb => { - cb(key, oldValue, newValue); + cb(key, oldValue, newValue, resolveMutation(oldObject, rawObj)); }); } - observer.setProp(key); + observer.setProp(key, resolveMutation(oldObject, rawObj)); } - - console.log('mutation from: ', JSON.parse(oldObject), ' to: ', ret); return ret; } diff --git a/libs/horizon/src/horizonx/store/StoreHandler.ts b/libs/horizon/src/horizonx/store/StoreHandler.ts index dec1a61a..42db626d 100644 --- a/libs/horizon/src/horizonx/store/StoreHandler.ts +++ b/libs/horizon/src/horizonx/store/StoreHandler.ts @@ -191,9 +191,10 @@ export function createStore, C extend store: storeObj, }); - storeObj.$subscribe(() => { + proxyObj.addListener(mutation => { devtools.emit(STATE_CHANGE, { store: storeObj, + mutation, }); }); diff --git a/libs/horizon/src/horizonx/types.d.ts b/libs/horizon/src/horizonx/types.d.ts index f1a88e56..eec4a826 100644 --- a/libs/horizon/src/horizonx/types.d.ts +++ b/libs/horizon/src/horizonx/types.d.ts @@ -16,13 +16,13 @@ export interface IObserver { useProp: (key: string | symbol) => void; - addListener: (listener: () => void) => void; + addListener: (listener: (mutation: any) => void) => void; - removeListener: (listener: () => void) => void; + removeListener: (listener: (mutation: any) => void) => void; - setProp: (key: string | symbol) => void; + setProp: (key: string | symbol, mutation: any) => void; - triggerChangeListeners: () => void; + triggerChangeListeners: (mutation: any) => void; triggerUpdate: (vNode: any) => void; @@ -42,13 +42,13 @@ export type StoreConfig, C extends Us }; export type UserActions = { - [K: string]: ActionFunction + [K: string]: ActionFunction; }; type ActionFunction = (this: StoreObj, state: S, ...args: any[]) => any; export type StoreActions> = { - [K in keyof A]: Action + [K in keyof A]: Action; }; type Action, S extends object> = ( @@ -61,8 +61,8 @@ export type StoreObj, C extends UserC $a: StoreActions; $c: UserComputedValues; $queue: QueuedStoreActions; - $subscribe: (listener: () => void) => void; - $unsubscribe: (listener: () => void) => void; + $subscribe: (listener: (mutation) => void) => void; + $unsubscribe: (listener: (mutation) => void) => void; } & { [K in keyof S]: S[K] } & { [K in keyof A]: Action } & { [K in keyof C]: ReturnType }; export type PlannedAction> = { @@ -74,11 +74,11 @@ export type PlannedAction> = { type RemoveFirstFromTuple = T['length'] extends 0 ? [] : ((...b: T) => void) extends (a, ...b: infer I) => void - ? I - : []; + ? I + : []; export type UserComputedValues = { - [K: string]: ComputedFunction + [K: string]: ComputedFunction; }; type ComputedFunction = (state: S) => any; @@ -89,9 +89,9 @@ export type AsyncAction, S extends object> = ( ) => Promise>; export type QueuedStoreActions> = { - [K in keyof A]: AsyncAction + [K in keyof A]: AsyncAction; }; export type ComputedValues> = { - [K in keyof C]: ReturnType + [K in keyof C]: ReturnType; }; diff --git a/scripts/__tests__/HorizonXText/StoreFunctionality/utils.test.js b/scripts/__tests__/HorizonXText/StoreFunctionality/utils.test.js new file mode 100644 index 00000000..7d1f786f --- /dev/null +++ b/scripts/__tests__/HorizonXText/StoreFunctionality/utils.test.js @@ -0,0 +1,80 @@ +import { resolveMutation } from '../../../../libs/horizon/src/horizonx/CommonUtils'; + +describe('Mutation resolve', () => { + it('should resolve mutation different types', () => { + const mutation = resolveMutation(null, 42); + + expect(mutation.mutation).toBe(true); + expect(mutation.from).toBe(null); + expect(mutation.to).toBe(42); + }); + + it('should resolve mutation same type types, different values', () => { + const mutation = resolveMutation(13, 42); + + expect(mutation.mutation).toBe(true); + expect(mutation.from).toBe(13); + expect(mutation.to).toBe(42); + }); + + it('should resolve mutation same type types, same values', () => { + const mutation = resolveMutation(42, 42); + + expect(mutation.mutation).toBe(false); + expect(Object.keys(mutation).length).toBe(1); + }); + + it('should resolve mutation same type types, same objects', () => { + const mutation = resolveMutation({ a: { b: { c: 1 } } }, { a: { b: { c: 1 } } }); + + expect(mutation.mutation).toBe(false); + }); + + it('should resolve mutation same type types, same array', () => { + const mutation = resolveMutation([1, 2, 3, 4, 5], [1, 2, 3, 4, 5]); + + expect(mutation.mutation).toBe(false); + }); + + it('should resolve mutation same type types, longer array', () => { + const mutation = resolveMutation([1, 2, 3, 4, 5], [1, 2, 3, 4, 5, 6]); + + expect(mutation.mutation).toBe(true); + expect(mutation.items[5].mutation).toBe(true); + expect(mutation.items[5].to).toBe(6); + }); + + it('should resolve mutation same type types, shorter array', () => { + const mutation = resolveMutation([1, 2, 3, 4, 5], [1, 2, 3, 4]); + + expect(mutation.mutation).toBe(true); + expect(mutation.items[4].mutation).toBe(true); + expect(mutation.items[4].from).toBe(5); + }); + + it('should resolve mutation same type types, changed array', () => { + const mutation = resolveMutation([1, 2, 3, 4, 5], [1, 2, 3, 4, 'a']); + + expect(mutation.mutation).toBe(true); + expect(mutation.items[4].mutation).toBe(true); + expect(mutation.items[4].from).toBe(5); + expect(mutation.items[4].to).toBe('a'); + }); + + it('should resolve mutation same type types, same object', () => { + const mutation = resolveMutation({ a: 1, b: 2 }, { a: 1, b: 2 }); + + console.log(mutation); + expect(mutation.mutation).toBe(false); + }); + + it('should resolve mutation same type types, changed object', () => { + const mutation = resolveMutation({ a: 1, b: 2, c: 3 }, { a: 1, c: 2 }); + + expect(mutation.mutation).toBe(true); + expect(mutation.attributes.a.mutation).toBe(false); + expect(mutation.attributes.b.mutation).toBe(true); + expect(mutation.attributes.b.from).toBe(2); + expect(mutation.attributes.c.to).toBe(2); + }); +}); From 0b11860c9d92ec06c84948f40a32252f6b0cec65 Mon Sep 17 00:00:00 2001 From: * <*> Date: Wed, 30 Nov 2022 23:00:15 +0800 Subject: [PATCH 12/38] Match-id-760cbbb23b2babb6aab29450b9834f032fec4914 --- .../src/horizonx/devtools/constants.ts | 2 + libs/horizon/src/horizonx/devtools/index.ts | 69 ++++++++++++++++++- libs/horizon/src/horizonx/proxy/Observer.ts | 27 +++++++- .../src/horizonx/proxy/ProxyHandler.ts | 2 +- .../proxy/handlers/ArrayProxyHandler.ts | 11 +-- .../proxy/handlers/CollectionProxyHandler.ts | 27 +++++--- .../proxy/handlers/ObjectProxyHandler.ts | 8 ++- .../src/horizonx/store/StoreHandler.ts | 23 +++++-- 8 files changed, 140 insertions(+), 29 deletions(-) diff --git a/libs/horizon/src/horizonx/devtools/constants.ts b/libs/horizon/src/horizonx/devtools/constants.ts index df41626f..3e105125 100644 --- a/libs/horizon/src/horizonx/devtools/constants.ts +++ b/libs/horizon/src/horizonx/devtools/constants.ts @@ -6,3 +6,5 @@ export const ACTION = 'horizonx action'; export const ACTION_QUEUED = 'horizonx action queued'; export const QUEUE_PENDING = 'horizonx queue pending'; export const QUEUE_FINISHED = 'horizonx queue finished'; +export const RENDER_TRIGGERED = 'horizonx render triggered'; +export const OBSERVED_COMPONENTS = 'horizonx observed components'; diff --git a/libs/horizon/src/horizonx/devtools/index.ts b/libs/horizon/src/horizonx/devtools/index.ts index 5aecbff3..7484fa88 100644 --- a/libs/horizon/src/horizonx/devtools/index.ts +++ b/libs/horizon/src/horizonx/devtools/index.ts @@ -1,5 +1,12 @@ +import { getStore, getAllStores } from '../store/StoreHandler'; +import { OBSERVED_COMPONENTS } from './constants'; + const sessionId = Date.now(); +export function isPanelActive() { + return window['__HORIZON_DEV_HOOK__']; +} + function makeStoreSnapshot({ type, data }) { const expanded = {}; Object.keys(data.store.$c).forEach(key => { @@ -40,9 +47,12 @@ function makeProxySnapshot(obj) { } export const devtools = { + getVNodeId: vNode => { + if (!isPanelActive()) return; + getVNodeId(vNode); + }, emit: (type, data) => { - if (!window['__HORIZON_DEV_HOOK__']) return; - console.log('store snapshot:', makeStoreSnapshot({ type, data })); + if (!isPanelActive()) return; window.postMessage({ type: 'HORIZON_DEV_TOOLS', payload: makeStoreSnapshot({ type, data }), @@ -50,3 +60,58 @@ export const devtools = { }); }, }; + +function getAffectedComponents() { + const allStores = getAllStores(); + const keys = Object.keys(allStores); + let res = {}; + keys.forEach(key => { + const subRes = new Set(); + const process = Array.from(allStores[key].$config.state._horizonObserver.keyVNodes.values()); + while (process.length) { + let pivot = process.shift(); + if (pivot?.tag) subRes.add(pivot); + if (pivot?.toString() === '[object Set]') Array.from(pivot).forEach(item => process.push(item)); + } + res[key] = Array.from(subRes).map(vnode => { + return { + name: vnode?.type + .toString() + .replace(/\{.*\}/gms, '{...}') + .replace('function ', ''), + nodeId: window.__HORIZON_DEV_HOOK__.getVnodeId(vnode), + }; + }); + }); + + return res; +} + +window.addEventListener('message', messageEvent => { + if (messageEvent.data.payload.type === 'horizonx request observed components') { + // get observed components + setTimeout(() => { + window.postMessage({ + type: 'HORIZON_DEV_TOOLS', + payload: { type: OBSERVED_COMPONENTS, data: getAffectedComponents() }, + from: 'dev tool hook', + }); + }, 100); + } + + if (messageEvent.data.payload.type === 'horizonx executue action') { + const data = messageEvent.data.payload.data; + const store = getStore(data.storeId); + if (!store?.[data.action]) { + } + + const action = store[data.action]; + const params = data.params; + action(...params).bind(store); + } +}); + +export function getVNodeId(vNode) { + window['__HORIZON_DEV_HOOK__'].send(); + return window['__HORIZON_DEV_HOOK__'].getVnodeId(vNode); +} diff --git a/libs/horizon/src/horizonx/proxy/Observer.ts b/libs/horizon/src/horizonx/proxy/Observer.ts index 2f3b15f8..adef63fc 100644 --- a/libs/horizon/src/horizonx/proxy/Observer.ts +++ b/libs/horizon/src/horizonx/proxy/Observer.ts @@ -16,6 +16,7 @@ import { launchUpdateFromVNode } from '../../renderer/TreeBuilder'; import { getProcessingVNode } from '../../renderer/GlobalVar'; import { VNode } from '../../renderer/vnode/VNode'; +import { devtools } from '../devtools'; export interface IObserver { useProp: (key: string) => void; @@ -89,7 +90,8 @@ export class Observer implements IObserver { this.triggerUpdate(vNode); }); - this.triggerChangeListeners(mutation); + // NOTE: mutations are different in dev and production. + this.triggerChangeListeners({ mutation, vNodes }); } triggerUpdate(vNode: VNode): void { @@ -109,8 +111,27 @@ export class Observer implements IObserver { this.listeners = this.listeners.filter(item => item != listener); } - triggerChangeListeners(mutation: any): void { - this.listeners.forEach(listener => listener(mutation)); + triggerChangeListeners({ mutation, vNodes }): void { + const nodesList = vNodes ? Array.from(vNodes) : []; + this.listeners.forEach(listener => + listener({ + mutation, + vNodes: nodesList.map(vNode => { + let realNode = vNode.realNode; + let searchedNode = vNode; + while (!realNode) { + searchedNode = searchedNode.child; + realNode = searchedNode.realNode; + } + return { + type: vNode?.type?.name, + id: devtools.getVNodeId(vNode), + path: vNode.path, + element: realNode?.outerHTML?.substr(0, 100), + }; + }), + }) + ); } // 触发所有使用的props的VNode更新 diff --git a/libs/horizon/src/horizonx/proxy/ProxyHandler.ts b/libs/horizon/src/horizonx/proxy/ProxyHandler.ts index ccc54ab1..fda6d653 100644 --- a/libs/horizon/src/horizonx/proxy/ProxyHandler.ts +++ b/libs/horizon/src/horizonx/proxy/ProxyHandler.ts @@ -27,7 +27,7 @@ const proxyMap = new WeakMap(); export const hookObserverMap = new WeakMap(); -export function createProxy(rawObj: any, isHookObserver = true): any { +export function createProxy(rawObj: any, id, isHookObserver = true): any { // 不是对象(是原始数据类型)不用代理 if (!(rawObj && isObject(rawObj))) { return rawObj; diff --git a/libs/horizon/src/horizonx/proxy/handlers/ArrayProxyHandler.ts b/libs/horizon/src/horizonx/proxy/handlers/ArrayProxyHandler.ts index 672253f8..9e9c2845 100644 --- a/libs/horizon/src/horizonx/proxy/handlers/ArrayProxyHandler.ts +++ b/libs/horizon/src/horizonx/proxy/handlers/ArrayProxyHandler.ts @@ -17,6 +17,7 @@ import { getObserver } from '../ProxyHandler'; import { isSame, isValidIntegerKey } from '../../CommonUtils'; import { get as objectGet } from './ObjectProxyHandler'; import { resolveMutation } from '../../CommonUtils'; +import { isPanelActive } from '../../devtools'; export function createArrayProxy(rawObj: any[]): any[] { const handle = { @@ -54,28 +55,30 @@ function set(rawObj: any[], key: string, value: any, receiver: any) { const oldLength = rawObj.length; const newValue = value; - const oldArray = JSON.parse(JSON.stringify(rawObj)); + const oldArray = isPanelActive() ? JSON.parse(JSON.stringify(rawObj)) : null; const ret = Reflect.set(rawObj, key, newValue, receiver); const newLength = rawObj.length; const observer = getObserver(rawObj); + const mutation = isPanelActive() ? resolveMutation(oldArray, rawObj) : { mutation: true, from: [], to: rawObj }; + if (!isSame(newValue, oldValue)) { // 值不一样,触发监听器 if (observer.watchers?.[key]) { observer.watchers[key].forEach(cb => { - cb(key, oldValue, newValue, resolveMutation(oldArray, rawObj)); + cb(key, oldValue, newValue, mutation); }); } // 触发属性变化 - observer.setProp(key, resolveMutation(oldValue, rawObj)); + observer.setProp(key, mutation); } if (oldLength !== newLength) { // 触发数组的大小变化 - observer.setProp('length', resolveMutation(oldValue, rawObj)); + observer.setProp('length', mutation); } return ret; diff --git a/libs/horizon/src/horizonx/proxy/handlers/CollectionProxyHandler.ts b/libs/horizon/src/horizonx/proxy/handlers/CollectionProxyHandler.ts index 74fa57bc..05174cc5 100644 --- a/libs/horizon/src/horizonx/proxy/handlers/CollectionProxyHandler.ts +++ b/libs/horizon/src/horizonx/proxy/handlers/CollectionProxyHandler.ts @@ -16,6 +16,7 @@ import { createProxy, getObserver, hookObserverMap } from '../ProxyHandler'; import { isMap, isWeakMap, isSame } from '../../CommonUtils'; import { resolveMutation } from '../../CommonUtils'; +import { isPanelActive } from '../../devtools'; const COLLECTION_CHANGE = '_collectionChange'; const handler = { @@ -91,18 +92,20 @@ function set( const valChange = !isSame(newValue, oldValue); const observer = getObserver(rawObj); + const mutation = isPanelActive() ? resolveMutation(oldValue, rawObj) : { mutation: true, from: null, to: rawObj }; + if (valChange || !rawObj.has(key)) { - observer.setProp(COLLECTION_CHANGE, resolveMutation(oldValue, rawObj)); + observer.setProp(COLLECTION_CHANGE, mutation); } if (valChange) { if (observer.watchers?.[key]) { observer.watchers[key].forEach(cb => { - cb(key, oldValue, newValue, resolveMutation(oldValue, rawObj)); + cb(key, oldValue, newValue, mutation); }); } - observer.setProp(key, resolveMutation(oldValue, rawObj)); + observer.setProp(key, mutation); } return rawObj; @@ -110,13 +113,16 @@ function set( // Set的add方法 function add(rawObj: { add: (any) => void; set: (string, any) => any; has: (any) => boolean }, value: any): Object { - const oldCollection = JSON.parse(JSON.stringify(rawObj)); + const oldCollection = isPanelActive() ? JSON.parse(JSON.stringify(rawObj)) : null; if (!rawObj.has(value)) { rawObj.add(value); const observer = getObserver(rawObj); - observer.setProp(value, resolveMutation(oldCollection, rawObj)); - observer.setProp(COLLECTION_CHANGE, resolveMutation(oldCollection, rawObj)); + const mutation = isPanelActive() + ? resolveMutation(oldCollection, rawObj) + : { mutation: true, from: null, to: rawObj }; + observer.setProp(value, mutation); + observer.setProp(COLLECTION_CHANGE, mutation); } return rawObj; @@ -140,13 +146,16 @@ function clear(rawObj: { size: number; clear: () => void }) { } function deleteFun(rawObj: { has: (key: any) => boolean; delete: (key: any) => void }, key: any) { - const oldCollection = JSON.parse(JSON.stringify(rawObj)); + const oldCollection = isPanelActive() ? JSON.parse(JSON.stringify(rawObj)) : null; if (rawObj.has(key)) { rawObj.delete(key); const observer = getObserver(rawObj); - observer.setProp(key, resolveMutation(oldCollection, rawObj)); - observer.setProp(COLLECTION_CHANGE, resolveMutation(oldCollection, rawObj)); + const mutation = isPanelActive() + ? resolveMutation(oldCollection, rawObj) + : { mutation: true, from: null, to: rawObj }; + observer.setProp(key, mutation); + observer.setProp(COLLECTION_CHANGE, mutation); return true; } diff --git a/libs/horizon/src/horizonx/proxy/handlers/ObjectProxyHandler.ts b/libs/horizon/src/horizonx/proxy/handlers/ObjectProxyHandler.ts index 9e0aac15..80da37fd 100644 --- a/libs/horizon/src/horizonx/proxy/handlers/ObjectProxyHandler.ts +++ b/libs/horizon/src/horizonx/proxy/handlers/ObjectProxyHandler.ts @@ -16,6 +16,7 @@ import { isSame, resolveMutation } from '../../CommonUtils'; import { createProxy, getObserver, hookObserverMap } from '../ProxyHandler'; import { OBSERVER_KEY } from '../../Constants'; +import { isPanelActive } from '../../devtools'; export function createObjectProxy(rawObj: T, singleLevel = false): ProxyHandler { const proxy = new Proxy(rawObj, { @@ -70,7 +71,7 @@ export function get(rawObj: object, key: string | symbol, receiver: any, singleL } export function set(rawObj: object, key: string, value: any, receiver: any): boolean { - const oldObject = JSON.parse(JSON.stringify(rawObj)); + const oldObject = isPanelActive() ? JSON.parse(JSON.stringify(rawObj)) : null; const observer = getObserver(rawObj); if (value && key == 'removeListener') { @@ -80,14 +81,15 @@ export function set(rawObj: object, key: string, value: any, receiver: any): boo const newValue = value; const ret = Reflect.set(rawObj, key, newValue, receiver); + const mutation = isPanelActive() ? resolveMutation(oldObject, rawObj) : { mutation: true, from: null, to: rawObj }; if (!isSame(newValue, oldValue)) { if (observer.watchers?.[key]) { observer.watchers[key].forEach(cb => { - cb(key, oldValue, newValue, resolveMutation(oldObject, rawObj)); + cb(key, oldValue, newValue, mutation); }); } - observer.setProp(key, resolveMutation(oldObject, rawObj)); + observer.setProp(key, mutation); } return ret; } diff --git a/libs/horizon/src/horizonx/store/StoreHandler.ts b/libs/horizon/src/horizonx/store/StoreHandler.ts index 42db626d..8b3aa154 100644 --- a/libs/horizon/src/horizonx/store/StoreHandler.ts +++ b/libs/horizon/src/horizonx/store/StoreHandler.ts @@ -14,7 +14,7 @@ */ import { useEffect, useRef } from '../../renderer/hooks/HookExternal'; -import { getProcessingVNode } from '../../renderer/GlobalVar'; +import { getProcessingVNode, getStartVNode } from '../../renderer/GlobalVar'; import { createProxy } from '../proxy/ProxyHandler'; import readonlyProxy from '../proxy/readonlyProxy'; import { Observer } from '../proxy/Observer'; @@ -60,7 +60,9 @@ export function createStore, C extend throw new Error('store obj must be pure object'); } - const proxyObj = createProxy(config.state, !config.options?.isReduxAdapter); + const id = config.id || idGenerator.get('UNNAMED_STORE'); + + const proxyObj = createProxy(config.state, id, !config.options?.isReduxAdapter); proxyObj.$pending = false; @@ -68,6 +70,7 @@ export function createStore, C extend const $queue: Partial> = {}; const $c: Partial> = {}; const storeObj = { + id, $s: proxyObj, $a: $a as StoreActions, $c: $c as ComputedValues, @@ -183,18 +186,16 @@ export function createStore, C extend }); } - if (config.id) { - storeMap.set(config.id, storeObj); - } + storeMap.set(id, storeObj); devtools.emit(INITIALIZED, { store: storeObj, }); - proxyObj.addListener(mutation => { + proxyObj.addListener(change => { devtools.emit(STATE_CHANGE, { store: storeObj, - mutation, + change, }); }); @@ -299,6 +300,14 @@ export function useStore, C extends U return storeObj as StoreObj; } +export function getStore(id: string) { + return storeMap.get(id); +} + +export function getAllStores() { + return Object.fromEntries(storeMap); +} + export function clearStore(id: string): void { storeMap.delete(id); } From 80ca0db9c351b5e6021fb5a59c388a84d7892a2b Mon Sep 17 00:00:00 2001 From: * <8> Date: Fri, 2 Dec 2022 18:59:38 +0800 Subject: [PATCH 13/38] Match-id-e80343297e5402927e0e2a30fa19743f685f1c2c --- libs/horizon/src/horizonx/devtools/index.ts | 26 ++++++++------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/libs/horizon/src/horizonx/devtools/index.ts b/libs/horizon/src/horizonx/devtools/index.ts index 7484fa88..9bb712b9 100644 --- a/libs/horizon/src/horizonx/devtools/index.ts +++ b/libs/horizon/src/horizonx/devtools/index.ts @@ -3,10 +3,12 @@ import { OBSERVED_COMPONENTS } from './constants'; const sessionId = Date.now(); +// this function is used to detect devtool connection export function isPanelActive() { return window['__HORIZON_DEV_HOOK__']; } +// serializes store and creates expanded object with baked-in containing current computed values function makeStoreSnapshot({ type, data }) { const expanded = {}; Object.keys(data.store.$c).forEach(key => { @@ -21,6 +23,7 @@ function makeStoreSnapshot({ type, data }) { return snapshot; } +// safely serializes variables containing values wrapped in Proxy object function makeProxySnapshot(obj) { let clone; try { @@ -47,10 +50,13 @@ function makeProxySnapshot(obj) { } export const devtools = { + // returns vNode id from horizon devtools getVNodeId: vNode => { if (!isPanelActive()) return; - getVNodeId(vNode); + window['__HORIZON_DEV_HOOK__'].send(); // update list first + return window['__HORIZON_DEV_HOOK__'].getVnodeId(vNode); }, + // sends horizonx devtool message to extension emit: (type, data) => { if (!isPanelActive()) return; window.postMessage({ @@ -61,6 +67,7 @@ export const devtools = { }, }; +// collects components that are dependant on horizonx store and their ids function getAffectedComponents() { const allStores = getAllStores(); const keys = Object.keys(allStores); @@ -87,6 +94,7 @@ function getAffectedComponents() { return res; } +// listens to messages from background window.addEventListener('message', messageEvent => { if (messageEvent.data.payload.type === 'horizonx request observed components') { // get observed components @@ -99,19 +107,5 @@ window.addEventListener('message', messageEvent => { }, 100); } - if (messageEvent.data.payload.type === 'horizonx executue action') { - const data = messageEvent.data.payload.data; - const store = getStore(data.storeId); - if (!store?.[data.action]) { - } - - const action = store[data.action]; - const params = data.params; - action(...params).bind(store); - } + }); - -export function getVNodeId(vNode) { - window['__HORIZON_DEV_HOOK__'].send(); - return window['__HORIZON_DEV_HOOK__'].getVnodeId(vNode); -} From 181e14e0f1204acdb620b183e9d3d0538e67a553 Mon Sep 17 00:00:00 2001 From: * <8> Date: Thu, 8 Dec 2022 21:15:09 +0800 Subject: [PATCH 14/38] Match-id-a91a7a5cd8ad285d742a7486e28a1da2a994cfa7 --- libs/horizon/src/horizonx/proxy/Observer.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libs/horizon/src/horizonx/proxy/Observer.ts b/libs/horizon/src/horizonx/proxy/Observer.ts index 0db792a0..c898c2b9 100644 --- a/libs/horizon/src/horizonx/proxy/Observer.ts +++ b/libs/horizon/src/horizonx/proxy/Observer.ts @@ -78,7 +78,9 @@ export class Observer implements IObserver { // 对象的属性被赋值时调用 setProp(key: string | symbol): void { const vNodes = this.keyVNodes.get(key); - vNodes?.forEach((vNode: VNode) => { + //NOTE: using Set directly can lead to deadlock + const vNodeArray = Array.from(vNodes || []); + vNodeArray?.forEach((vNode: VNode) => { if (vNode.isStoreChange) { // VNode已经被触发过,不再重复触发 return; From 75e24f26e03264d2dda7931daf16291dd1dc7ba6 Mon Sep 17 00:00:00 2001 From: * <8> Date: Thu, 8 Dec 2022 21:19:21 +0800 Subject: [PATCH 15/38] Match-id-2f0b8623c7ad7dda87cc0913e6702ddc6569c16c --- libs/horizon/src/horizonx/devtools/index.ts | 13 +++++++++++-- libs/horizon/src/horizonx/proxy/Observer.ts | 4 +++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/libs/horizon/src/horizonx/devtools/index.ts b/libs/horizon/src/horizonx/devtools/index.ts index 9bb712b9..e7f3514e 100644 --- a/libs/horizon/src/horizonx/devtools/index.ts +++ b/libs/horizon/src/horizonx/devtools/index.ts @@ -96,7 +96,7 @@ function getAffectedComponents() { // listens to messages from background window.addEventListener('message', messageEvent => { - if (messageEvent.data.payload.type === 'horizonx request observed components') { + if (messageEvent?.data?.payload?.type === 'horizonx request observed components') { // get observed components setTimeout(() => { window.postMessage({ @@ -107,5 +107,14 @@ window.addEventListener('message', messageEvent => { }, 100); } - + // executes store action + if (messageEvent.data.payload.type === 'horizonx executue action') { + const data = messageEvent.data.payload.data; + const store = getStore(data.storeId); + if (!store?.[data.action]) return; + + const action = store[data.action]; + const params = data.params; + action(...params); + } }); diff --git a/libs/horizon/src/horizonx/proxy/Observer.ts b/libs/horizon/src/horizonx/proxy/Observer.ts index adef63fc..b1aa43d7 100644 --- a/libs/horizon/src/horizonx/proxy/Observer.ts +++ b/libs/horizon/src/horizonx/proxy/Observer.ts @@ -79,7 +79,9 @@ export class Observer implements IObserver { // 对象的属性被赋值时调用 setProp(key: string | symbol, mutation: any): void { const vNodes = this.keyVNodes.get(key); - vNodes?.forEach((vNode: VNode) => { + //NOTE: using Set directly can lead to deadlock + const vNodeArray = Array.from(vNodes || []); + vNodeArray?.forEach((vNode: VNode) => { if (vNode.isStoreChange) { // VNode已经被触发过,不再重复触发 return; From 9ac6225f3ccc3bb723c803c460d17ca67620fef1 Mon Sep 17 00:00:00 2001 From: * <8> Date: Tue, 3 Jan 2023 17:57:34 +0800 Subject: [PATCH 16/38] Match-id-8ef71c7cfe07396edb4a3bc4819b13211abff46f --- CHANGELOG.md | 3 +++ libs/horizon/package.json | 2 +- scripts/rollup/rollup.config.js | 8 ++++++-- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b1b9cfbc..74cdf1c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## 0.0.29 (2023-01-03) +- **CI**: 生成态输出文件改为horiozn.producion.min.js + ## 0.0.26 (2022-11-09) - **CI**: 包信息同步CMC diff --git a/libs/horizon/package.json b/libs/horizon/package.json index 0658232f..6960612e 100644 --- a/libs/horizon/package.json +++ b/libs/horizon/package.json @@ -4,7 +4,7 @@ "keywords": [ "horizon" ], - "version": "0.0.26", + "version": "0.0.29", "homepage": "", "bugs": "", "main": "index.js", diff --git a/scripts/rollup/rollup.config.js b/scripts/rollup/rollup.config.js index efe2aabd..05bd691e 100644 --- a/scripts/rollup/rollup.config.js +++ b/scripts/rollup/rollup.config.js @@ -38,6 +38,10 @@ if (!fs.existsSync(outDir)) { const outputResolve = (...p) => path.resolve(outDir, ...p); +function getOutputName(mode) { + return mode === 'production' ? `horizon.${mode}.min.js` : `horizon.${mode}.js`; +} + function genConfig(mode) { const isDev = mode === 'development'; const sourcemap = isDev ? 'inline' : false; @@ -45,12 +49,12 @@ function genConfig(mode) { input: path.resolve(libDir, 'index.ts'), output: [ { - file: outputResolve('cjs', `horizon.${mode}.js`), + file: outputResolve('cjs', getOutputName(mode)), sourcemap, format: 'cjs', }, { - file: outputResolve('umd', `horizon.${mode}.js`), + file: outputResolve('umd', getOutputName(mode)), sourcemap, name: 'Horizon', format: 'umd', From 69ebc90fc06af27353a45790c22f23749d6e4163 Mon Sep 17 00:00:00 2001 From: * <8> Date: Tue, 3 Jan 2023 20:13:42 +0800 Subject: [PATCH 17/38] Match-id-73ed656be14649c6992f6d3c2786429fa30c531f --- CHANGELOG.md | 2 +- libs/horizon/package.json | 2 +- libs/horizon/src/horizonx/devtools/index.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 74cdf1c1..77f50501 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## 0.0.29 (2023-01-03) +## 0.0.30 (2023-01-03) - **CI**: 生成态输出文件改为horiozn.producion.min.js ## 0.0.26 (2022-11-09) diff --git a/libs/horizon/package.json b/libs/horizon/package.json index 6960612e..f55ef7f3 100644 --- a/libs/horizon/package.json +++ b/libs/horizon/package.json @@ -4,7 +4,7 @@ "keywords": [ "horizon" ], - "version": "0.0.29", + "version": "0.0.30", "homepage": "", "bugs": "", "main": "index.js", diff --git a/libs/horizon/src/horizonx/devtools/index.ts b/libs/horizon/src/horizonx/devtools/index.ts index e7f3514e..f49deaaa 100644 --- a/libs/horizon/src/horizonx/devtools/index.ts +++ b/libs/horizon/src/horizonx/devtools/index.ts @@ -108,7 +108,7 @@ window.addEventListener('message', messageEvent => { } // executes store action - if (messageEvent.data.payload.type === 'horizonx executue action') { + if (messageEvent.data?.payload?.type === 'horizonx executue action') { const data = messageEvent.data.payload.data; const store = getStore(data.storeId); if (!store?.[data.action]) return; From e68c314a7ce70dc057ec1bf982e80009889a9672 Mon Sep 17 00:00:00 2001 From: * <8> Date: Tue, 3 Jan 2023 20:46:02 +0800 Subject: [PATCH 18/38] Match-id-8a478cb738af6a424913924a76ddbf200cd4224a --- CHANGELOG.md | 2 +- libs/horizon/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 77f50501..5c4cc304 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## 0.0.30 (2023-01-03) +## 0.0.31 (2023-01-03) - **CI**: 生成态输出文件改为horiozn.producion.min.js ## 0.0.26 (2022-11-09) diff --git a/libs/horizon/package.json b/libs/horizon/package.json index f55ef7f3..ac6da789 100644 --- a/libs/horizon/package.json +++ b/libs/horizon/package.json @@ -4,7 +4,7 @@ "keywords": [ "horizon" ], - "version": "0.0.30", + "version": "0.0.31", "homepage": "", "bugs": "", "main": "index.js", From bec0024e993ac770980ef3f8b07bce4a2abc0527 Mon Sep 17 00:00:00 2001 From: * <8> Date: Wed, 4 Jan 2023 10:43:54 +0800 Subject: [PATCH 19/38] Match-id-13a6245d00057e47102443ca931241ba424f83cc --- .cloudbuild/release.sh | 2 +- libs/horizon/npm/index.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.cloudbuild/release.sh b/.cloudbuild/release.sh index 55565e19..11171071 100644 --- a/.cloudbuild/release.sh +++ b/.cloudbuild/release.sh @@ -20,7 +20,7 @@ if [ -n "${releaseVersion}" ] ; then cd umd # umd生产包多暴露全局名HorizonDOM # 以解决webpack的externals react-dom和react都指向Horizon时,webpack随机使用key名造成源码交付问题 - sed -i '$a window.HorizonDOM = window.Horizon;' horizon.production.js + sed -i '$a window.HorizonDOM = window.Horizon;' horizon.production.min.js cd - # 写入新版本号 diff --git a/libs/horizon/npm/index.js b/libs/horizon/npm/index.js index c2cf8335..5e212be2 100644 --- a/libs/horizon/npm/index.js +++ b/libs/horizon/npm/index.js @@ -16,7 +16,7 @@ 'use strict'; if (process.env.NODE_ENV === 'production') { - module.exports = require('./cjs/horizon.production.js'); + module.exports = require('./cjs/horizon.production.min.js'); } else { module.exports = require('./cjs/horizon.development.js'); } From ab6e01f7669e932c8851a3e8bd18b6074b33fdd6 Mon Sep 17 00:00:00 2001 From: * <8> Date: Wed, 4 Jan 2023 10:52:40 +0800 Subject: [PATCH 20/38] Match-id-b6520f0900dbec69028e76cb82027fe73b384e9c --- CHANGELOG.md | 2 +- libs/horizon/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c4cc304..b4cad439 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## 0.0.31 (2023-01-03) +## 0.0.32 (2023-01-04) - **CI**: 生成态输出文件改为horiozn.producion.min.js ## 0.0.26 (2022-11-09) diff --git a/libs/horizon/package.json b/libs/horizon/package.json index ac6da789..4127a769 100644 --- a/libs/horizon/package.json +++ b/libs/horizon/package.json @@ -4,7 +4,7 @@ "keywords": [ "horizon" ], - "version": "0.0.31", + "version": "0.0.32", "homepage": "", "bugs": "", "main": "index.js", From 4712e8a621f3178f30b70a3d90eee6f3d7dbe6d1 Mon Sep 17 00:00:00 2001 From: * <*> Date: Tue, 10 Jan 2023 19:55:01 +0800 Subject: [PATCH 21/38] Match-id-8c8edc92392dcae4fa155fe80ecae735f6f4d0fc --- libs/horizon/src/renderer/components/BaseClassComponent.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/horizon/src/renderer/components/BaseClassComponent.ts b/libs/horizon/src/renderer/components/BaseClassComponent.ts index fff07c8d..8a931e04 100644 --- a/libs/horizon/src/renderer/components/BaseClassComponent.ts +++ b/libs/horizon/src/renderer/components/BaseClassComponent.ts @@ -29,7 +29,7 @@ class Component { this.context = context; } - setState(state: S) { + setState(state: S, callback?: any) { if (isDev) { console.error('Cant not call `this.setState` in the constructor of class component, it will do nothing'); } From 1c3c678883fa3aeb5afb8c987429bc9151326a5e Mon Sep 17 00:00:00 2001 From: * <*> Date: Tue, 10 Jan 2023 20:09:54 +0800 Subject: [PATCH 22/38] Match-id-9952e9ebc64f02cfbab95841eee1b1788348e9f6 --- libs/horizon/src/renderer/Types.ts | 2 ++ libs/horizon/src/renderer/UpdateHandler.ts | 4 +--- libs/horizon/src/renderer/components/BaseClassComponent.ts | 4 +++- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/libs/horizon/src/renderer/Types.ts b/libs/horizon/src/renderer/Types.ts index 1503fee9..91b6bfbd 100644 --- a/libs/horizon/src/renderer/Types.ts +++ b/libs/horizon/src/renderer/Types.ts @@ -77,3 +77,5 @@ export type Source = { fileName: string; lineNumber: number; }; + +export type Callback = () => void; diff --git a/libs/horizon/src/renderer/UpdateHandler.ts b/libs/horizon/src/renderer/UpdateHandler.ts index c906bd1c..5fd17bb1 100644 --- a/libs/horizon/src/renderer/UpdateHandler.ts +++ b/libs/horizon/src/renderer/UpdateHandler.ts @@ -13,7 +13,7 @@ * See the Mulan PSL v2 for more details. */ -import type { VNode } from './Types'; +import type { VNode, Callback } from './Types'; import { FlagUtils, ShouldCapture } from './vnode/VNodeFlags'; export type Update = { @@ -22,8 +22,6 @@ export type Update = { callback: Callback | null; }; -export type Callback = () => any; - export type Updates = Array | null; export enum UpdateState { diff --git a/libs/horizon/src/renderer/components/BaseClassComponent.ts b/libs/horizon/src/renderer/components/BaseClassComponent.ts index 8a931e04..de4ff351 100644 --- a/libs/horizon/src/renderer/components/BaseClassComponent.ts +++ b/libs/horizon/src/renderer/components/BaseClassComponent.ts @@ -13,6 +13,8 @@ * See the Mulan PSL v2 for more details. */ +import {Callback} from '../Types'; + /** * Component的api setState和forceUpdate在实例生成阶段实现 */ @@ -29,7 +31,7 @@ class Component { this.context = context; } - setState(state: S, callback?: any) { + setState(state: S, callback?: Callback) { if (isDev) { console.error('Cant not call `this.setState` in the constructor of class component, it will do nothing'); } From 5fd818fa33989025f59a067a5f16b1fd34e63309 Mon Sep 17 00:00:00 2001 From: * <8> Date: Wed, 11 Jan 2023 17:40:02 +0800 Subject: [PATCH 23/38] Match-id-7f5f612322b6696d330d0c879bc995b8fc36a65c --- CHANGELOG.md | 3 +++ libs/horizon/package.json | 2 +- libs/horizon/src/horizonx/devtools/index.ts | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b4cad439..d71ca87c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## 0.0.33 (2023-01-11) +- **horizonX-devtool**: 修复IE中报错 + ## 0.0.32 (2023-01-04) - **CI**: 生成态输出文件改为horiozn.producion.min.js diff --git a/libs/horizon/package.json b/libs/horizon/package.json index 4127a769..8b7332a0 100644 --- a/libs/horizon/package.json +++ b/libs/horizon/package.json @@ -4,7 +4,7 @@ "keywords": [ "horizon" ], - "version": "0.0.32", + "version": "0.0.33", "homepage": "", "bugs": "", "main": "index.js", diff --git a/libs/horizon/src/horizonx/devtools/index.ts b/libs/horizon/src/horizonx/devtools/index.ts index f49deaaa..35f924e6 100644 --- a/libs/horizon/src/horizonx/devtools/index.ts +++ b/libs/horizon/src/horizonx/devtools/index.ts @@ -84,7 +84,7 @@ function getAffectedComponents() { return { name: vnode?.type .toString() - .replace(/\{.*\}/gms, '{...}') + .replace(/\{.*\}/, '{...}') .replace('function ', ''), nodeId: window.__HORIZON_DEV_HOOK__.getVnodeId(vnode), }; From 67d39ec1002a1695d2a211602f018b6c8f18d129 Mon Sep 17 00:00:00 2001 From: * <*> Date: Thu, 12 Jan 2023 17:47:54 +0800 Subject: [PATCH 24/38] Match-id-7cdd603ac18c622fdf10e6266e101785657b3f47 --- libs/horizon/jsx-runtime.ts | 24 ++++++++++++++++ libs/horizon/src/external/JSXElement.ts | 9 ++++++ scripts/rollup/rollup.config.js | 37 +++++++++++++++++-------- 3 files changed, 59 insertions(+), 11 deletions(-) create mode 100644 libs/horizon/jsx-runtime.ts diff --git a/libs/horizon/jsx-runtime.ts b/libs/horizon/jsx-runtime.ts new file mode 100644 index 00000000..40b7fd86 --- /dev/null +++ b/libs/horizon/jsx-runtime.ts @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2020 Huawei Technologies Co.,Ltd. + * + * openGauss is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { + TYPE_FRAGMENT as Fragment, +} from './src/external/JSXElementType'; +import { jsx } from './src/external/JSXElement'; + +export { + jsx, + Fragment +}; diff --git a/libs/horizon/src/external/JSXElement.ts b/libs/horizon/src/external/JSXElement.ts index ebb43d18..a4cf50b7 100644 --- a/libs/horizon/src/external/JSXElement.ts +++ b/libs/horizon/src/external/JSXElement.ts @@ -107,3 +107,12 @@ export function cloneElement(element, setting, ...children) { export function isValidElement(element) { return !!(element && element.vtype === TYPE_COMMON_ELEMENT); } + +// 兼容高版本的babel编译方式 +export function jsx(type, setting, key) { + if (setting.key === undefined && key !== undefined) { + setting.key = key; + } + + return buildElement(false, type, setting, []); +} diff --git a/scripts/rollup/rollup.config.js b/scripts/rollup/rollup.config.js index 05bd691e..57e17279 100644 --- a/scripts/rollup/rollup.config.js +++ b/scripts/rollup/rollup.config.js @@ -37,6 +37,18 @@ if (!fs.existsSync(outDir)) { } const outputResolve = (...p) => path.resolve(outDir, ...p); +const BasicPlugins = [ + nodeResolve({ + extensions, + modulesOnly: true, + }), + babel({ + exclude: 'node_modules/**', + configFile: path.join(__dirname, '../../babel.config.js'), + babelHelpers: 'runtime', + extensions, + }) +]; function getOutputName(mode) { return mode === 'production' ? `horizon.${mode}.min.js` : `horizon.${mode}.js`; @@ -61,16 +73,7 @@ function genConfig(mode) { }, ], plugins: [ - nodeResolve({ - extensions, - modulesOnly: true, - }), - babel({ - exclude: 'node_modules/**', - configFile: path.join(__dirname, '../../babel.config.js'), - babelHelpers: 'runtime', - extensions, - }), + ...BasicPlugins, replace({ values: { 'process.env.NODE_ENV': `"${mode}"`, @@ -96,4 +99,16 @@ function genConfig(mode) { }; } -export default [genConfig('development'), genConfig('production')]; +function genJSXRuntimeConfig() { + return { + input: path.resolve(libDir, 'jsx-runtime.ts'), + output: [ + { + file: outputResolve('jsx-runtime.js'), + format: 'cjs', + } + ], + plugins: BasicPlugins + }; +} +export default [genConfig('development'), genConfig('production'), genJSXRuntimeConfig()]; From cadaabfd3b863bb76438f2a980548ee115ab78d1 Mon Sep 17 00:00:00 2001 From: * <*> Date: Thu, 12 Jan 2023 17:48:28 +0800 Subject: [PATCH 25/38] Match-id-1f58ff819dfbd01d0b80fd9bfaa198bcb73f0b50 --- .../src/renderer/taskExecutor/BrowserAsync.ts | 33 ++++++++++++++----- .../src/renderer/taskExecutor/TaskExecutor.ts | 7 +--- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/libs/horizon/src/renderer/taskExecutor/BrowserAsync.ts b/libs/horizon/src/renderer/taskExecutor/BrowserAsync.ts index 28e17a84..2aeead9f 100644 --- a/libs/horizon/src/renderer/taskExecutor/BrowserAsync.ts +++ b/libs/horizon/src/renderer/taskExecutor/BrowserAsync.ts @@ -19,11 +19,9 @@ let isMessageLoopRunning = false; let browserCallback = null; -const { port1, port2 } = new MessageChannel(); - -export function isOverTime() { - return false; -} +let port1 = null; +let port2 = null; +let isTestRuntime = false; // 1、设置deadline;2、回调TaskExecutor传过来的browserCallback const callRenderTasks = () => { @@ -41,21 +39,38 @@ const callRenderTasks = () => { browserCallback = null; } else { // 还有task,继续调用 - port2.postMessage(null); + asyncCall(); } } catch (error) { - port2.postMessage(null); + asyncCall(); throw error; } }; -port1.onmessage = callRenderTasks; +if (typeof MessageChannel === 'function') { + const mc = new MessageChannel(); + port1 = mc.port1; + port1.onmessage = callRenderTasks; + port2 = mc.port2; +} else { + // 测试环境没有 MessageChannel + isTestRuntime = true; +} + +function asyncCall() { + if (isTestRuntime) { + setTimeout(callRenderTasks, 0); + } else { + port2.postMessage(null); + } +} export function requestBrowserCallback(callback) { browserCallback = callback; if (!isMessageLoopRunning) { isMessageLoopRunning = true; - port2.postMessage(null); + asyncCall(); } } + diff --git a/libs/horizon/src/renderer/taskExecutor/TaskExecutor.ts b/libs/horizon/src/renderer/taskExecutor/TaskExecutor.ts index 8613cfca..ac5ce9b3 100644 --- a/libs/horizon/src/renderer/taskExecutor/TaskExecutor.ts +++ b/libs/horizon/src/renderer/taskExecutor/TaskExecutor.ts @@ -18,7 +18,7 @@ */ import { Node } from '../taskExecutor/TaskQueue'; -import { requestBrowserCallback, isOverTime } from './BrowserAsync'; +import { requestBrowserCallback } from './BrowserAsync'; import { add, shift, first, remove } from './TaskQueue'; @@ -44,11 +44,6 @@ function callTasks() { // 循环执行task while (task !== null) { - if (isOverTime()) { - // 超过了deadline - break; - } - const callback = task.callback; if (callback !== null) { task.callback = null; From 673a63c40daa3afa44b344fba59517e010299333 Mon Sep 17 00:00:00 2001 From: * <*> Date: Fri, 13 Jan 2023 10:38:17 +0800 Subject: [PATCH 26/38] Match-id-a6a5ea6fabf8fd82802778a00a09be2442e7a0b5 --- libs/horizon/src/renderer/taskExecutor/BrowserAsync.ts | 4 ++++ libs/horizon/src/renderer/taskExecutor/TaskExecutor.ts | 7 ++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/libs/horizon/src/renderer/taskExecutor/BrowserAsync.ts b/libs/horizon/src/renderer/taskExecutor/BrowserAsync.ts index 2aeead9f..6da28613 100644 --- a/libs/horizon/src/renderer/taskExecutor/BrowserAsync.ts +++ b/libs/horizon/src/renderer/taskExecutor/BrowserAsync.ts @@ -23,6 +23,10 @@ let port1 = null; let port2 = null; let isTestRuntime = false; +export function isOverTime() { + return false; +} + // 1、设置deadline;2、回调TaskExecutor传过来的browserCallback const callRenderTasks = () => { if (browserCallback === null) { diff --git a/libs/horizon/src/renderer/taskExecutor/TaskExecutor.ts b/libs/horizon/src/renderer/taskExecutor/TaskExecutor.ts index ac5ce9b3..8613cfca 100644 --- a/libs/horizon/src/renderer/taskExecutor/TaskExecutor.ts +++ b/libs/horizon/src/renderer/taskExecutor/TaskExecutor.ts @@ -18,7 +18,7 @@ */ import { Node } from '../taskExecutor/TaskQueue'; -import { requestBrowserCallback } from './BrowserAsync'; +import { requestBrowserCallback, isOverTime } from './BrowserAsync'; import { add, shift, first, remove } from './TaskQueue'; @@ -44,6 +44,11 @@ function callTasks() { // 循环执行task while (task !== null) { + if (isOverTime()) { + // 超过了deadline + break; + } + const callback = task.callback; if (callback !== null) { task.callback = null; From cdc872be3f6e7c3cf1f3a3b65023569bb6fa5e2d Mon Sep 17 00:00:00 2001 From: * <*> Date: Mon, 16 Jan 2023 20:04:55 +0800 Subject: [PATCH 27/38] Match-id-a889c6b66e320203447be4d1bcf1011b502b27c4 --- libs/horizon/index.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/libs/horizon/index.ts b/libs/horizon/index.ts index 59045fbb..f71aa9e6 100644 --- a/libs/horizon/index.ts +++ b/libs/horizon/index.ts @@ -65,6 +65,11 @@ const act = fun => { asyncUpdates(fun); callRenderQueueImmediate(); runAsyncEffects(); + + // 兼容返回Promise + return new Promise((resolve, reject) => { + resolve(null); + }); }; import { From 2aaad576fd2eabefa8eccee6847c0198cf31772a Mon Sep 17 00:00:00 2001 From: * <*> Date: Mon, 16 Jan 2023 20:29:01 +0800 Subject: [PATCH 28/38] Match-id-13432ea7f850d2d66d77c7fc83bbaf071859351f --- libs/horizon/index.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/libs/horizon/index.ts b/libs/horizon/index.ts index f71aa9e6..737861a7 100644 --- a/libs/horizon/index.ts +++ b/libs/horizon/index.ts @@ -67,9 +67,7 @@ const act = fun => { runAsyncEffects(); // 兼容返回Promise - return new Promise((resolve, reject) => { - resolve(null); - }); + return Promise.resolve(); }; import { From f3c0cf9a05a99a47ecbf69396fb6acb880f49425 Mon Sep 17 00:00:00 2001 From: * <*> Date: Thu, 19 Jan 2023 16:56:53 +0800 Subject: [PATCH 29/38] Match-id-470122e11f1af7f936f3b0991fd467d8fd693eda --- CHANGELOG.md | 4 ++ libs/horizon/global.d.ts | 1 + libs/horizon/index.ts | 14 +---- libs/horizon/package.json | 2 +- libs/horizon/src/external/JSXElement.ts | 16 +++++- libs/horizon/src/external/TestUtil.ts | 69 +++++++++++++++++++++++ libs/horizon/src/renderer/ErrorHandler.ts | 2 +- 7 files changed, 91 insertions(+), 17 deletions(-) create mode 100644 libs/horizon/src/external/TestUtil.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index d71ca87c..2eb75f8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.0.34 (2023-01-11) +- **core**: #95 新增jsx接口 +- **core**: #96 #97 fix testing-library 的UT错误 + ## 0.0.33 (2023-01-11) - **horizonX-devtool**: 修复IE中报错 diff --git a/libs/horizon/global.d.ts b/libs/horizon/global.d.ts index 34c32508..9da3e079 100644 --- a/libs/horizon/global.d.ts +++ b/libs/horizon/global.d.ts @@ -19,3 +19,4 @@ declare var isDev: boolean; declare var isTest: boolean; declare const __VERSION__: string; +declare var setImmediate: Function; diff --git a/libs/horizon/index.ts b/libs/horizon/index.ts index 737861a7..90d62f6e 100644 --- a/libs/horizon/index.ts +++ b/libs/horizon/index.ts @@ -42,9 +42,6 @@ import { useState, useDebugValue, } from './src/renderer/hooks/HookExternal'; -import { asyncUpdates } from './src/renderer/TreeBuilder'; -import { callRenderQueueImmediate } from './src/renderer/taskExecutor/RenderQueue'; -import { runAsyncEffects } from './src/renderer/submit/HookEffectHandler'; import { isContextProvider, isContextConsumer, @@ -59,16 +56,7 @@ import { import { createStore, useStore, clearStore } from './src/horizonx/store/StoreHandler'; import * as reduxAdapter from './src/horizonx/adapters/redux'; import { watch } from './src/horizonx/proxy/watch'; - -// act用于测试,作用是:如果fun触发了刷新(包含了异步刷新),可以保证在act后面的代码是在刷新完成后才执行。 -const act = fun => { - asyncUpdates(fun); - callRenderQueueImmediate(); - runAsyncEffects(); - - // 兼容返回Promise - return Promise.resolve(); -}; +import { act } from './src/external/TestUtil'; import { render, diff --git a/libs/horizon/package.json b/libs/horizon/package.json index 8b7332a0..aa78f321 100644 --- a/libs/horizon/package.json +++ b/libs/horizon/package.json @@ -4,7 +4,7 @@ "keywords": [ "horizon" ], - "version": "0.0.33", + "version": "0.0.34", "homepage": "", "bugs": "", "main": "index.js", diff --git a/libs/horizon/src/external/JSXElement.ts b/libs/horizon/src/external/JSXElement.ts index a4cf50b7..614733be 100644 --- a/libs/horizon/src/external/JSXElement.ts +++ b/libs/horizon/src/external/JSXElement.ts @@ -25,10 +25,10 @@ import { Source } from '../renderer/Types'; * props 其他常规属性 */ export function JSXElement(type, key, ref, vNode, props, source: Source | null) { - return { + const ele = { // 元素标识符 vtype: TYPE_COMMON_ELEMENT, - src: isDev ? source : null, + src: null, // 属于元素的内置属性 type: type, @@ -39,6 +39,18 @@ export function JSXElement(type, key, ref, vNode, props, source: Source | null) // 所属的class组件 belongClassVNode: vNode, }; + + if (isDev) { + // 为了test判断两个JSXElement对象是否相等时忽略src属性,需要设置src的enumerable为false + Object.defineProperty(ele, 'src', { + configurable: false, + enumerable: false, + writable: false, + value: source, + }); + } + + return ele; } function isValidKey(key) { diff --git a/libs/horizon/src/external/TestUtil.ts b/libs/horizon/src/external/TestUtil.ts new file mode 100644 index 00000000..6eccbbe5 --- /dev/null +++ b/libs/horizon/src/external/TestUtil.ts @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2020 Huawei Technologies Co.,Ltd. + * + * openGauss is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + + +import {asyncUpdates} from '../renderer/TreeBuilder'; +import {callRenderQueueImmediate} from '../renderer/taskExecutor/RenderQueue'; +import {runAsyncEffects} from '../renderer/submit/HookEffectHandler'; +import {isPromise} from '../renderer/ErrorHandler'; + +// act用于测试,作用是:如果fun触发了刷新(包含了异步刷新),可以保证在act后面的代码是在刷新完成后才执行。 +function act(fun) { + const funRet = asyncUpdates(fun); + + callRenderQueueImmediate(); + runAsyncEffects(); + // effects可能产生刷新任务,这里再执行一次 + callRenderQueueImmediate(); + + // 如果fun返回的是Promise + if (isPromise(funRet)) { + // testing-library会返回Promise + return { + then(resolve, reject) { + funRet.then( + () => { + if (typeof setImmediate === 'function') { + // 通过setImmediate回调,用于等待业务的setTimeout完成 + setImmediate(() => { + callRenderQueueImmediate(); + runAsyncEffects(); + resolve(); + }); + } else { + callRenderQueueImmediate(); + runAsyncEffects(); + resolve(); + } + }, + err => { + reject(err); + }, + ); + }, + }; + } else { + return { + then(resolve) { + resolve(); + }, + }; + } + +} + +export { + act +} diff --git a/libs/horizon/src/renderer/ErrorHandler.ts b/libs/horizon/src/renderer/ErrorHandler.ts index af115325..7ba23cec 100644 --- a/libs/horizon/src/renderer/ErrorHandler.ts +++ b/libs/horizon/src/renderer/ErrorHandler.ts @@ -72,7 +72,7 @@ function createClassErrorUpdate(vNode: VNode, error: any): Update { } return update; } -function isPromise(error: any): error is PromiseType { +export function isPromise(error: any): error is PromiseType { return error !== null && typeof error === 'object' && typeof error.then === 'function'; } // 处理capture和bubble阶段抛出的错误 From 8c13846d2def98ba9c0404487cce00514e2c6d9f Mon Sep 17 00:00:00 2001 From: * <*> Date: Thu, 19 Jan 2023 17:44:36 +0800 Subject: [PATCH 30/38] Match-id-bb9faf6ae4ba719bc43b183906c421855724b0b7 --- libs/horizon/src/external/TestUtil.ts | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/libs/horizon/src/external/TestUtil.ts b/libs/horizon/src/external/TestUtil.ts index 6eccbbe5..26d87660 100644 --- a/libs/horizon/src/external/TestUtil.ts +++ b/libs/horizon/src/external/TestUtil.ts @@ -19,14 +19,15 @@ import {callRenderQueueImmediate} from '../renderer/taskExecutor/RenderQueue'; import {runAsyncEffects} from '../renderer/submit/HookEffectHandler'; import {isPromise} from '../renderer/ErrorHandler'; +interface Thenable { + then(resolve: (val?: any) => void, reject: (err: any) => void): void; +} + // act用于测试,作用是:如果fun触发了刷新(包含了异步刷新),可以保证在act后面的代码是在刷新完成后才执行。 -function act(fun) { +function act(fun: () => void | Thenable): Thenable { const funRet = asyncUpdates(fun); - callRenderQueueImmediate(); - runAsyncEffects(); - // effects可能产生刷新任务,这里再执行一次 - callRenderQueueImmediate(); + callRenderQueue(); // 如果fun返回的是Promise if (isPromise(funRet)) { @@ -38,13 +39,11 @@ function act(fun) { if (typeof setImmediate === 'function') { // 通过setImmediate回调,用于等待业务的setTimeout完成 setImmediate(() => { - callRenderQueueImmediate(); - runAsyncEffects(); + callRenderQueue(); resolve(); }); } else { - callRenderQueueImmediate(); - runAsyncEffects(); + callRenderQueue(); resolve(); } }, @@ -61,9 +60,15 @@ function act(fun) { }, }; } +} +function callRenderQueue() { + callRenderQueueImmediate(); + runAsyncEffects(); + // effects可能产生刷新任务,这里再执行一次 + callRenderQueueImmediate(); } export { act -} +}; From efdc5f5b0a2f66a5187fa758320bbb9a463a2876 Mon Sep 17 00:00:00 2001 From: * <*> Date: Sat, 28 Jan 2023 09:54:07 +0800 Subject: [PATCH 31/38] Match-id-a4d79ffb0f91ea94e459845a9b689e6a7125798f --- CHANGELOG.md | 5 ++++- libs/horizon/package.json | 2 +- libs/horizon/src/external/JSXElement.ts | 11 +++++++++-- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2eb75f8d..542c6277 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,7 @@ -## 0.0.34 (2023-01-11) +## 0.0.35 (2023-01-28) +- **core**: 在 cloneDeep JSXElement 的时候会出现死循环 + +## 0.0.34 (2023-01-19) - **core**: #95 新增jsx接口 - **core**: #96 #97 fix testing-library 的UT错误 diff --git a/libs/horizon/package.json b/libs/horizon/package.json index aa78f321..39d033c1 100644 --- a/libs/horizon/package.json +++ b/libs/horizon/package.json @@ -4,7 +4,7 @@ "keywords": [ "horizon" ], - "version": "0.0.34", + "version": "0.0.35", "homepage": "", "bugs": "", "main": "index.js", diff --git a/libs/horizon/src/external/JSXElement.ts b/libs/horizon/src/external/JSXElement.ts index 614733be..3de212b3 100644 --- a/libs/horizon/src/external/JSXElement.ts +++ b/libs/horizon/src/external/JSXElement.ts @@ -37,11 +37,18 @@ export function JSXElement(type, key, ref, vNode, props, source: Source | null) props: props, // 所属的class组件 - belongClassVNode: vNode, + belongClassVNode: null, }; + // 在 cloneDeep JSXElement 的时候会出现死循环,需要设置belongClassVNode的enumerable为false + Object.defineProperty(ele, 'belongClassVNode', { + configurable: false, + enumerable: false, + value: vNode, + }); + if (isDev) { - // 为了test判断两个JSXElement对象是否相等时忽略src属性,需要设置src的enumerable为false + // 为了test判断两个 JSXElement 对象是否相等时忽略src属性,需要设置src的enumerable为false Object.defineProperty(ele, 'src', { configurable: false, enumerable: false, From 9758a9981e496634e50c41806b7416442cbdb95d Mon Sep 17 00:00:00 2001 From: * <*> Date: Mon, 30 Jan 2023 11:57:45 +0800 Subject: [PATCH 32/38] Match-id-0792823163a9df4d31c3755f318c7ceb06153781 --- CHANGELOG.md | 3 +++ libs/horizon/package.json | 2 +- libs/horizon/src/dom/DOMOperator.ts | 10 ++++++++-- libs/horizon/src/dom/utils/DomCreator.ts | 6 +++--- libs/horizon/src/renderer/RootStack.ts | 1 + 5 files changed, 16 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 542c6277..08d2e879 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## 0.0.36 (2023-01-30) +- **core**: #100 horizon从上层页面透传到iframe页面里使用,创建的dom元素instanceof HTMLElement为false + ## 0.0.35 (2023-01-28) - **core**: 在 cloneDeep JSXElement 的时候会出现死循环 diff --git a/libs/horizon/package.json b/libs/horizon/package.json index 39d033c1..20c8dfc2 100644 --- a/libs/horizon/package.json +++ b/libs/horizon/package.json @@ -4,7 +4,7 @@ "keywords": [ "horizon" ], - "version": "0.0.35", + "version": "0.0.36", "homepage": "", "bugs": "", "main": "index.js", diff --git a/libs/horizon/src/dom/DOMOperator.ts b/libs/horizon/src/dom/DOMOperator.ts index ed3047b8..a7abe22d 100644 --- a/libs/horizon/src/dom/DOMOperator.ts +++ b/libs/horizon/src/dom/DOMOperator.ts @@ -16,7 +16,7 @@ import { saveVNode, updateVNodeProps } from './DOMInternalKeys'; import { createDom } from './utils/DomCreator'; import { getSelectionInfo, resetSelectionRange, SelectionData } from './SelectionRangeHandler'; -import { shouldAutoFocus } from './utils/Common'; +import { isDocument, shouldAutoFocus } from './utils/Common'; import { NSS } from './utils/DomCreator'; import { adjustStyleValue } from './DOMPropertiesHandler/StyleHandler'; import type { VNode } from '../renderer/Types'; @@ -26,6 +26,7 @@ import { isNativeElement, validateProps } from './validators/ValidateProps'; import { watchValueChange } from './valueHandler/ValueChangeHandler'; import { DomComponent, DomText } from '../renderer/vnode/VNodeTags'; import { updateCommonProp } from './DOMPropertiesHandler/UpdateCommonProp'; +import {getCurrentRoot} from '../renderer/RootStack'; export type Props = Record & { autoFocus?: boolean; @@ -70,7 +71,12 @@ export function resetAfterSubmit(): void { // 创建 DOM 对象 export function newDom(tagName: string, props: Props, parentNamespace: string, vNode: VNode): Element { - const dom: Element = createDom(tagName, parentNamespace); + // document取值于treeRoot对应的DOM的ownerDocument。 + // 解决:在iframe中使用top的horizon时,horizon在创建DOM时用到的document并不是iframe的document,而是top中的document的问题。 + const rootDom = getCurrentRoot().realNode; + const doc = isDocument(rootDom) ? rootDom : rootDom.ownerDocument; + + const dom: Element = createDom(tagName, parentNamespace, doc); // 将 vNode 节点挂到 DOM 对象上 saveVNode(vNode, dom); // 将属性挂到 DOM 对象上 diff --git a/libs/horizon/src/dom/utils/DomCreator.ts b/libs/horizon/src/dom/utils/DomCreator.ts index 3eb2d1ca..26fe2840 100644 --- a/libs/horizon/src/dom/utils/DomCreator.ts +++ b/libs/horizon/src/dom/utils/DomCreator.ts @@ -20,15 +20,15 @@ export const NSS = { }; // 创建DOM元素 -export function createDom(tagName: string, parentNamespace: string): Element { +export function createDom(tagName: string, parentNamespace: string, doc: Document): Element { let dom: Element; const selfNamespace = NSS[tagName] || NSS.html; const ns = parentNamespace !== NSS.html ? parentNamespace : selfNamespace; if (ns !== NSS.html) { - dom = document.createElementNS(ns, tagName); + dom = doc.createElementNS(ns, tagName); } else { - dom = document.createElement(tagName); + dom = doc.createElement(tagName); } return dom; } diff --git a/libs/horizon/src/renderer/RootStack.ts b/libs/horizon/src/renderer/RootStack.ts index 110cad72..6a793a0f 100644 --- a/libs/horizon/src/renderer/RootStack.ts +++ b/libs/horizon/src/renderer/RootStack.ts @@ -14,6 +14,7 @@ */ import { VNode } from './vnode/VNode'; + const currentRootStack: VNode[] = []; export function getCurrentRoot() { return currentRootStack[currentRootStack.length - 1]; From 71db7f0f6ba43721471219fcc3b8e94c1314b735 Mon Sep 17 00:00:00 2001 From: * <*> Date: Tue, 31 Jan 2023 16:57:38 +0800 Subject: [PATCH 33/38] Match-id-5bad0bb66ce628a366c9f96c499b0f7c84ec9b8e --- libs/horizon/jsx-runtime.ts | 3 +- scripts/rollup/rollup.config.js | 63 +++++++++++++++++++-------------- 2 files changed, 38 insertions(+), 28 deletions(-) diff --git a/libs/horizon/jsx-runtime.ts b/libs/horizon/jsx-runtime.ts index 40b7fd86..f130dc06 100644 --- a/libs/horizon/jsx-runtime.ts +++ b/libs/horizon/jsx-runtime.ts @@ -16,9 +16,10 @@ import { TYPE_FRAGMENT as Fragment, } from './src/external/JSXElementType'; -import { jsx } from './src/external/JSXElement'; +import { jsx, jsx as jsxs } from './src/external/JSXElement'; export { jsx, + jsxs, Fragment }; diff --git a/scripts/rollup/rollup.config.js b/scripts/rollup/rollup.config.js index 57e17279..4e2e7cfd 100644 --- a/scripts/rollup/rollup.config.js +++ b/scripts/rollup/rollup.config.js @@ -37,26 +37,42 @@ if (!fs.existsSync(outDir)) { } const outputResolve = (...p) => path.resolve(outDir, ...p); -const BasicPlugins = [ - nodeResolve({ - extensions, - modulesOnly: true, - }), - babel({ - exclude: 'node_modules/**', - configFile: path.join(__dirname, '../../babel.config.js'), - babelHelpers: 'runtime', - extensions, - }) -]; + +const isDev = (mode) => { + return mode === 'development'; +} + +const getBasicPlugins = (mode) => { + return [ + nodeResolve({ + extensions, + modulesOnly: true, + }), + babel({ + exclude: 'node_modules/**', + configFile: path.join(__dirname, '../../babel.config.js'), + babelHelpers: 'runtime', + extensions, + }), + replace({ + values: { + 'process.env.NODE_ENV': `"${mode}"`, + isDev: isDev(mode).toString(), + isTest: false, + __VERSION__: `"${horizonVersion}"`, + }, + preventAssignment: true, + }), + ]; +} + function getOutputName(mode) { return mode === 'production' ? `horizon.${mode}.min.js` : `horizon.${mode}.js`; } function genConfig(mode) { - const isDev = mode === 'development'; - const sourcemap = isDev ? 'inline' : false; + const sourcemap = isDev(mode) ? 'inline' : false; return { input: path.resolve(libDir, 'index.ts'), output: [ @@ -73,16 +89,7 @@ function genConfig(mode) { }, ], plugins: [ - ...BasicPlugins, - replace({ - values: { - 'process.env.NODE_ENV': `"${mode}"`, - isDev: isDev.toString(), - isTest: false, - __VERSION__: `"${horizonVersion}"`, - }, - preventAssignment: true, - }), + ...getBasicPlugins(mode), execute('npm run build-types'), mode === 'production' && terser(), copy([ @@ -99,7 +106,7 @@ function genConfig(mode) { }; } -function genJSXRuntimeConfig() { +function genJSXRuntimeConfig(mode) { return { input: path.resolve(libDir, 'jsx-runtime.ts'), output: [ @@ -108,7 +115,9 @@ function genJSXRuntimeConfig() { format: 'cjs', } ], - plugins: BasicPlugins + plugins: [ + ...getBasicPlugins(mode) + ] }; } -export default [genConfig('development'), genConfig('production'), genJSXRuntimeConfig()]; +export default [genConfig('development'), genConfig('production'), genJSXRuntimeConfig('')]; From a87766e0f05102d5475739b9fd5db0887bc731ae Mon Sep 17 00:00:00 2001 From: * <*> Date: Tue, 31 Jan 2023 17:48:27 +0800 Subject: [PATCH 34/38] Match-id-074f59ec0016e87e4f2c57740fb016379ad8b90c --- CHANGELOG.md | 3 +++ libs/horizon/package.json | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 08d2e879..a6b0e35c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## 0.0.37 (2023-01-31) +- **core**: 增加jsxs方法 + ## 0.0.36 (2023-01-30) - **core**: #100 horizon从上层页面透传到iframe页面里使用,创建的dom元素instanceof HTMLElement为false diff --git a/libs/horizon/package.json b/libs/horizon/package.json index 20c8dfc2..2c9aeeff 100644 --- a/libs/horizon/package.json +++ b/libs/horizon/package.json @@ -4,7 +4,7 @@ "keywords": [ "horizon" ], - "version": "0.0.36", + "version": "0.0.37", "homepage": "", "bugs": "", "main": "index.js", From de2328827bda114086679f6bfe225f4ad673f6eb Mon Sep 17 00:00:00 2001 From: * <*> Date: Wed, 1 Feb 2023 11:07:23 +0800 Subject: [PATCH 35/38] Match-id-64d8ec9cf05e0338f4d584bf7221bcfb50cbe776 --- CHANGELOG.md | 3 +++ libs/horizon/index.ts | 4 ++++ libs/horizon/package.json | 2 +- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a6b0e35c..12d81d5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## 0.0.38 (2023-02-01) +- **core**: 增加flushSync接口 + ## 0.0.37 (2023-01-31) - **core**: 增加jsxs方法 diff --git a/libs/horizon/index.ts b/libs/horizon/index.ts index 90d62f6e..5fffa7c9 100644 --- a/libs/horizon/index.ts +++ b/libs/horizon/index.ts @@ -66,6 +66,8 @@ import { unmountComponentAtNode, } from './src/dom/DOMExternal'; +import { syncUpdates as flushSync } from './src/renderer/TreeBuilder'; + const Horizon = { Children, createRef, @@ -98,6 +100,7 @@ const Horizon = { findDOMNode, unmountComponentAtNode, act, + flushSync, createStore, useStore, clearStore, @@ -147,6 +150,7 @@ export { findDOMNode, unmountComponentAtNode, act, + flushSync, // 状态管理器HorizonX接口 createStore, useStore, diff --git a/libs/horizon/package.json b/libs/horizon/package.json index 2c9aeeff..c9a179bc 100644 --- a/libs/horizon/package.json +++ b/libs/horizon/package.json @@ -4,7 +4,7 @@ "keywords": [ "horizon" ], - "version": "0.0.37", + "version": "0.0.38", "homepage": "", "bugs": "", "main": "index.js", From 3124afc8f8981097e945016317fdbaf8cd4b91c2 Mon Sep 17 00:00:00 2001 From: * <*> Date: Thu, 2 Feb 2023 07:12:36 +0800 Subject: [PATCH 36/38] Match-id-8a23de5aec1505a2c6c8e028a30493badb356e08 --- libs/horizon/src/horizonx/devtools/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/horizon/src/horizonx/devtools/index.ts b/libs/horizon/src/horizonx/devtools/index.ts index 35f924e6..7ea843ea 100644 --- a/libs/horizon/src/horizonx/devtools/index.ts +++ b/libs/horizon/src/horizonx/devtools/index.ts @@ -52,7 +52,7 @@ function makeProxySnapshot(obj) { export const devtools = { // returns vNode id from horizon devtools getVNodeId: vNode => { - if (!isPanelActive()) return; + if (!isPanelActive() || !window['__HORIZON_DEV_HOOK__'].getVnodeId) return; window['__HORIZON_DEV_HOOK__'].send(); // update list first return window['__HORIZON_DEV_HOOK__'].getVnodeId(vNode); }, From 8e80651ee5523bd4c07c1adf636d7bf9b8ee74d0 Mon Sep 17 00:00:00 2001 From: * <*> Date: Thu, 9 Feb 2023 14:51:56 +0800 Subject: [PATCH 37/38] Match-id-5c4291c86acb903dd0fa11acc884291cf546f67d --- .cloudbuild/test.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.cloudbuild/test.yml b/.cloudbuild/test.yml index 0593afd8..2681012f 100644 --- a/.cloudbuild/test.yml +++ b/.cloudbuild/test.yml @@ -22,7 +22,8 @@ steps: - checkout: path: horizon-core - gitlab: - url: https://szv-open.codehub.huawei.com/innersource/shanhai/wutong/react/horizon-test.git + url: https://szv-open.codehub.huawei.com/innersource/fenghuang/horizon/horizon-test.git + branch: one_tree_dev path: horizon-test BUILD: From 47e7baaa4865c775ff08eb09242fc9bfb4c36b64 Mon Sep 17 00:00:00 2001 From: * <*> Date: Wed, 15 Feb 2023 15:11:46 +0800 Subject: [PATCH 38/38] Match-id-a8d664cc1a7599bfd5612227fe90df71d89342af --- .cloudbuild/test.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.cloudbuild/test.yml b/.cloudbuild/test.yml index 2681012f..a5c7242f 100644 --- a/.cloudbuild/test.yml +++ b/.cloudbuild/test.yml @@ -23,8 +23,7 @@ steps: path: horizon-core - gitlab: url: https://szv-open.codehub.huawei.com/innersource/fenghuang/horizon/horizon-test.git - - branch: one_tree_dev + branch: master path: horizon-test BUILD: - build_execute: