Match-id-fd8dc79257296a4416c4601bff170c98a696b094
This commit is contained in:
parent
d466afb2c6
commit
0da7446962
|
@ -27,10 +27,12 @@ import {
|
||||||
useState,
|
useState,
|
||||||
useDebugValue
|
useDebugValue
|
||||||
} from './src/renderer/hooks/HookExternal';
|
} from './src/renderer/hooks/HookExternal';
|
||||||
import { launchUpdateFromVNode as _launchUpdateFromVNode, asyncUpdates } from './src/renderer/TreeBuilder';
|
import { asyncUpdates } from './src/renderer/TreeBuilder';
|
||||||
import { callRenderQueueImmediate } from './src/renderer/taskExecutor/RenderQueue';
|
import { callRenderQueueImmediate } from './src/renderer/taskExecutor/RenderQueue';
|
||||||
import { runAsyncEffects } from './src/renderer/submit/HookEffectHandler';
|
import { runAsyncEffects } from './src/renderer/submit/HookEffectHandler';
|
||||||
import { getProcessingVNode as _getProcessingVNode } from './src/renderer/GlobalVar';
|
|
||||||
|
import { createStore, useStore, clearStore } from './src/horizonx/store/StoreHandler';
|
||||||
|
import * as reduxAdapter from './src/horizonx/adapters/redux';
|
||||||
|
|
||||||
// act用于测试,作用是:如果fun触发了刷新(包含了异步刷新),可以保证在act后面的代码是在刷新完成后才执行。
|
// act用于测试,作用是:如果fun触发了刷新(包含了异步刷新),可以保证在act后面的代码是在刷新完成后才执行。
|
||||||
const act = fun => {
|
const act = fun => {
|
||||||
|
@ -79,8 +81,10 @@ const Horizon = {
|
||||||
findDOMNode,
|
findDOMNode,
|
||||||
unmountComponentAtNode,
|
unmountComponentAtNode,
|
||||||
act,
|
act,
|
||||||
_launchUpdateFromVNode,
|
createStore,
|
||||||
_getProcessingVNode,
|
useStore,
|
||||||
|
clearStore,
|
||||||
|
reduxAdapter,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const version = __VERSION__;
|
export const version = __VERSION__;
|
||||||
|
@ -116,9 +120,11 @@ export {
|
||||||
findDOMNode,
|
findDOMNode,
|
||||||
unmountComponentAtNode,
|
unmountComponentAtNode,
|
||||||
act,
|
act,
|
||||||
// 暂时给HorizonX使用
|
// 状态管理器HorizonX接口
|
||||||
_launchUpdateFromVNode,
|
createStore,
|
||||||
_getProcessingVNode,
|
useStore,
|
||||||
|
clearStore,
|
||||||
|
reduxAdapter,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Horizon;
|
export default Horizon;
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
export function isObject(obj) {
|
||||||
|
const type = typeof obj;
|
||||||
|
return obj != null && (type === 'object' || type === 'function');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isSet(obj) {
|
||||||
|
return obj != null && (Object.prototype.toString.call(obj) === '[object Set]' || obj.constructor === Set);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isWeakSet(obj) {
|
||||||
|
return obj != null && (Object.prototype.toString.call(obj) === '[object WeakSet]' || obj.constructor === WeakSet);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isMap(obj) {
|
||||||
|
return obj != null && (Object.prototype.toString.call(obj) === '[object Map]' || obj.constructor === Map);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isWeakMap(obj) {
|
||||||
|
return obj != null && (Object.prototype.toString.call(obj) === '[object WeakMap]' || obj.constructor === WeakMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isArray(obj) {
|
||||||
|
return Object.prototype.toString.call(obj) === '[object Array]';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isCollection(obj) {
|
||||||
|
return isSet(obj) || isWeakSet(obj) || isMap(obj) || isWeakMap(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isString(obj) {
|
||||||
|
return typeof obj === 'string';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isValidIntegerKey(key) {
|
||||||
|
return isString(key) && key !== 'NaN' && key[0] !== '-' && String(parseInt(key, 10)) === key;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const noop = () => {};
|
||||||
|
|
||||||
|
export function isSame(x, y) {
|
||||||
|
if (!(typeof Object.is === 'function')) {
|
||||||
|
if (x === y) {
|
||||||
|
// +0 != -0
|
||||||
|
return x !== 0 || 1 / x === 1 / y;
|
||||||
|
} else {
|
||||||
|
// NaN == NaN
|
||||||
|
return x !== x && y !== y;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Object.is(x, y);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
// The two constants must be the same as those in horizon.
|
||||||
|
export const FunctionComponent = 'FunctionComponent';
|
||||||
|
export const ClassComponent = 'ClassComponent';
|
|
@ -0,0 +1,124 @@
|
||||||
|
import { createStore as createStoreX } from '../store/StoreHandler';
|
||||||
|
|
||||||
|
import { ReduxStoreHandler, ReduxAction, ReduxMiddleware } from '../types';
|
||||||
|
|
||||||
|
export { thunk } from './reduxThunk';
|
||||||
|
|
||||||
|
export { Provider, useSelector, useStore, useDispatch, connect, createSelectorHook, createDispatchHook } from './reduxReact';
|
||||||
|
|
||||||
|
type Reducer = (state: any, action: ReduxAction) => any;
|
||||||
|
|
||||||
|
export function createStore(reducer: Reducer, preloadedState: any, enhancers): ReduxStoreHandler {
|
||||||
|
const store = createStoreX({
|
||||||
|
id: 'defaultStore',
|
||||||
|
state: { stateWrapper: preloadedState },
|
||||||
|
actions: {
|
||||||
|
dispatch: (state: { stateWrapper?: any }, action) => {
|
||||||
|
let result;
|
||||||
|
if (state.stateWrapper !== undefined && state.stateWrapper !== null) {
|
||||||
|
result = reducer(state.stateWrapper, action);
|
||||||
|
} else {
|
||||||
|
result = reducer(undefined, action);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result === undefined) {
|
||||||
|
return;
|
||||||
|
} // NOTE: reducer should never return undefined, in this case, do not change state
|
||||||
|
state.stateWrapper = result;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
suppressHooks: true,
|
||||||
|
},
|
||||||
|
})();
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
reducer,
|
||||||
|
getState: function() {
|
||||||
|
return store.$state.stateWrapper;
|
||||||
|
},
|
||||||
|
subscribe: listener => {
|
||||||
|
store.$subscribe(listener);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
store.$unsubscribe(listener);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
replaceReducer: newReducer => {
|
||||||
|
reducer = newReducer;
|
||||||
|
},
|
||||||
|
_horizonXstore: store,
|
||||||
|
dispatch: store.$actions.dispatch,
|
||||||
|
};
|
||||||
|
|
||||||
|
enhancers && enhancers(result);
|
||||||
|
|
||||||
|
result.dispatch({ type: 'HorizonX' });
|
||||||
|
|
||||||
|
store.reduxHandler = result;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function combineReducers(reducers: { [key: string]: Reducer }): Reducer {
|
||||||
|
return (state = {}, action) => {
|
||||||
|
const newState = {};
|
||||||
|
Object.entries(reducers).forEach(([key, reducer]) => {
|
||||||
|
newState[key] = reducer(state[key], action);
|
||||||
|
});
|
||||||
|
return newState;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyMiddleware(...middlewares: ReduxMiddleware[]): (store: ReduxStoreHandler) => void {
|
||||||
|
return store => {
|
||||||
|
return applyMiddlewares(store, middlewares);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyMiddlewares(store: ReduxStoreHandler, middlewares: ReduxMiddleware[]): void {
|
||||||
|
middlewares = middlewares.slice();
|
||||||
|
middlewares.reverse();
|
||||||
|
let dispatch = store.dispatch;
|
||||||
|
middlewares.forEach(middleware => {
|
||||||
|
dispatch = middleware(store)(dispatch);
|
||||||
|
});
|
||||||
|
store.dispatch = dispatch;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActionCreator = (...params: any[]) => ReduxAction;
|
||||||
|
type ActionCreators = { [key: string]: ActionCreator };
|
||||||
|
export type BoundActionCreator = (...params: any[]) => void;
|
||||||
|
type BoundActionCreators = { [key: string]: BoundActionCreator };
|
||||||
|
type Dispatch = (action) => any;
|
||||||
|
|
||||||
|
export function bindActionCreators(actionCreators: ActionCreators, dispatch: Dispatch): BoundActionCreators {
|
||||||
|
const boundActionCreators = {};
|
||||||
|
Object.entries(actionCreators).forEach(([key, value]) => {
|
||||||
|
boundActionCreators[key] = (...args) => {
|
||||||
|
dispatch(value(...args));
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return boundActionCreators;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function compose(middlewares: ReduxMiddleware[]) {
|
||||||
|
return (store: ReduxStoreHandler, extraArgument: any) => {
|
||||||
|
let val;
|
||||||
|
middlewares.reverse().forEach((middleware: ReduxMiddleware, index) => {
|
||||||
|
if (!index) {
|
||||||
|
val = middleware(store, extraArgument);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
val = middleware(val);
|
||||||
|
});
|
||||||
|
return val;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// HorizonX batches updates by default, this function is only for backwards compatibility
|
||||||
|
export function batch(fn: () => void) {
|
||||||
|
fn();
|
||||||
|
}
|
|
@ -0,0 +1,166 @@
|
||||||
|
// @ts-ignore
|
||||||
|
import { useState, useContext, useEffect, useRef } from '../../renderer/hooks/HookExternal';
|
||||||
|
import { createContext } from '../../renderer/components/context/CreateContext';
|
||||||
|
import { createElement } from '../../external/JSXElement';
|
||||||
|
import { BoundActionCreator } from './redux';
|
||||||
|
import { ReduxAction, ReduxStoreHandler } from '../types';
|
||||||
|
|
||||||
|
const DefaultContext = createContext();
|
||||||
|
type Context = typeof DefaultContext;
|
||||||
|
|
||||||
|
export function Provider({
|
||||||
|
store,
|
||||||
|
context = DefaultContext,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
store: ReduxStoreHandler;
|
||||||
|
context: Context;
|
||||||
|
children?: any[];
|
||||||
|
}) {
|
||||||
|
const Context = context; // NOTE: bind redux API to horizon API requires this renaming;
|
||||||
|
return createElement(Context.Provider, { value: store }, children);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createStoreHook(context: Context) {
|
||||||
|
return () => {
|
||||||
|
return useContext(context);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createSelectorHook(context: Context): (selector: (any) => any) => any {
|
||||||
|
const store = createStoreHook(context)();
|
||||||
|
return function(selector = state => state) {
|
||||||
|
const [b, fr] = useState(false);
|
||||||
|
|
||||||
|
const listener = () => {
|
||||||
|
fr(!b);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const unsubscribe = store.subscribe(listener);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsubscribe(listener);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return selector(store.getState());
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createDispatchHook(context: Context): BoundActionCreator {
|
||||||
|
const store = createStoreHook(context)();
|
||||||
|
return function() {
|
||||||
|
return action => {
|
||||||
|
this.dispatch(action);
|
||||||
|
};
|
||||||
|
}.bind(store);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useSelector = selector => {
|
||||||
|
return createSelectorHook(DefaultContext)(selector);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useDispatch = () => {
|
||||||
|
return createDispatchHook(DefaultContext)();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useStore = () => {
|
||||||
|
return createStoreHook(DefaultContext)();
|
||||||
|
};
|
||||||
|
|
||||||
|
// function shallowCompare(a,b){
|
||||||
|
// return Object.keys(a).length === Object.keys(b).length &&
|
||||||
|
// Object.keys(a).every(key => a[key] === b[key]);
|
||||||
|
// }
|
||||||
|
|
||||||
|
//TODO: implement options
|
||||||
|
// context?: Object,
|
||||||
|
// areStatesEqual?: Function, :)
|
||||||
|
// areOwnPropsEqual?: Function,
|
||||||
|
// areStatePropsEqual?: Function,
|
||||||
|
// areMergedPropsEqual?: Function,
|
||||||
|
// forwardRef?: boolean,
|
||||||
|
// const defaultOptions = {
|
||||||
|
// areStatesEqual: shallowCompare,
|
||||||
|
// areOwnPropsEqual: shallowCompare,
|
||||||
|
// areStatePropsEqual: shallowCompare,
|
||||||
|
// 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,
|
||||||
|
options?: {
|
||||||
|
areStatesEqual?: (oldState: any, newState: any) => boolean;
|
||||||
|
context?: any; // TODO: type this
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
if (!options) {
|
||||||
|
options = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return Component => {
|
||||||
|
const useStore = createStoreHook(options.context || DefaultContext);
|
||||||
|
|
||||||
|
function Wrapper(props) {
|
||||||
|
const [f, forceReload] = useState(true);
|
||||||
|
|
||||||
|
const store = useStore();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const unsubscribe = store.subscribe(() => forceReload(!f));
|
||||||
|
() => {
|
||||||
|
unsubscribe(() => forceReload(!f));
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const previous = useRef({
|
||||||
|
state: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
let mappedState;
|
||||||
|
if (options.areStatesEqual) {
|
||||||
|
if (options.areStatesEqual(previous.current.state, store.getState())) {
|
||||||
|
mappedState = previous.current.mappedState;
|
||||||
|
} else {
|
||||||
|
mappedState = mapStateToProps ? mapStateToProps(store.getState(), props) : {};
|
||||||
|
previous.current.mappedState = mappedState;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
mappedState = mapStateToProps ? mapStateToProps(store.getState(), props) : {};
|
||||||
|
previous.current.mappedState = mappedState;
|
||||||
|
}
|
||||||
|
let mappedDispatch: { dispatch?: (action) => void } = {};
|
||||||
|
if (mapDispatchToProps) {
|
||||||
|
if (typeof mapDispatchToProps === 'object') {
|
||||||
|
Object.entries(mapDispatchToProps).forEach(([key, value]) => {
|
||||||
|
mappedDispatch[key] = (...args) => {
|
||||||
|
store.dispatch(value(...args));
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
mappedDispatch = mapDispatchToProps(store.dispatch, props);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
mappedDispatch.dispatch = store.dispatch;
|
||||||
|
}
|
||||||
|
const mergedProps = (
|
||||||
|
mergeProps ||
|
||||||
|
((state, dispatch, originalProps) => {
|
||||||
|
return { ...state, ...dispatch, ...originalProps };
|
||||||
|
})
|
||||||
|
)(mappedState, mappedDispatch, props);
|
||||||
|
|
||||||
|
previous.current.state = store.getState();
|
||||||
|
|
||||||
|
const node = createElement(Component, mergedProps);
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Wrapper;
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { ReduxStoreHandler, ReduxAction, ReduxMiddleware } from '../types';
|
||||||
|
|
||||||
|
function createThunkMiddleware(extraArgument?: any): ReduxMiddleware {
|
||||||
|
return (store: ReduxStoreHandler) => (next: (action: ReduxAction) => any) => (
|
||||||
|
action:
|
||||||
|
| ReduxAction
|
||||||
|
| ((dispatch: (action: ReduxAction) => void, store: ReduxStoreHandler, extraArgument?: any) => any)
|
||||||
|
) => {
|
||||||
|
// This gets called for every action you dispatch.
|
||||||
|
// If it's a function, call it.
|
||||||
|
if (typeof action === 'function') {
|
||||||
|
return action(store.dispatch, store.getState.bind(store), extraArgument);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, just continue processing this action as usual
|
||||||
|
return next(action);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const thunk = createThunkMiddleware();
|
||||||
|
// @ts-ignore
|
||||||
|
thunk.withExtraArgument = createThunkMiddleware;
|
|
@ -0,0 +1,47 @@
|
||||||
|
// TODO: implement vNode type
|
||||||
|
|
||||||
|
import {IObserver} from '../types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 一个对象(对象、数组、集合)对应一个Observer
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export class HooklessObserver implements IObserver {
|
||||||
|
|
||||||
|
listeners = [];
|
||||||
|
vNodeKeys: null;
|
||||||
|
keyVNodes: null;
|
||||||
|
|
||||||
|
useProp(key: string): void {
|
||||||
|
}
|
||||||
|
|
||||||
|
addListener(listener: () => void) {
|
||||||
|
this.listeners.push(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeListener(listener: () => void) {
|
||||||
|
this.listeners = this.listeners.filter(item => item != listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
setProp(key: string): void {
|
||||||
|
this.triggerChangeListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
triggerChangeListeners(): void {
|
||||||
|
this.listeners.forEach(listener => {
|
||||||
|
if (!listener) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
listener();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
triggerUpdate(vNode): void {
|
||||||
|
}
|
||||||
|
|
||||||
|
allChange(): void {
|
||||||
|
}
|
||||||
|
|
||||||
|
clearByVNode(vNode): void {
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,101 @@
|
||||||
|
/**
|
||||||
|
* 一个对象(对象、数组、集合)对应一个Observer
|
||||||
|
*/
|
||||||
|
|
||||||
|
//@ts-ignore
|
||||||
|
import { launchUpdateFromVNode } from '../../renderer/TreeBuilder';
|
||||||
|
import { getProcessingVNode } from '../../renderer/GlobalVar';
|
||||||
|
import { VNode } from '../../renderer/vnode/VNode';
|
||||||
|
import { IObserver } from '../types';
|
||||||
|
|
||||||
|
export class Observer implements IObserver {
|
||||||
|
vNodeKeys = new WeakMap();
|
||||||
|
|
||||||
|
keyVNodes = new Map();
|
||||||
|
|
||||||
|
listeners = [];
|
||||||
|
|
||||||
|
useProp(key: string): void {
|
||||||
|
const processingVNode = getProcessingVNode();
|
||||||
|
if (processingVNode === null || !processingVNode.observers) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// vNode -> Observers
|
||||||
|
processingVNode.observers.add(this);
|
||||||
|
|
||||||
|
// key -> vNodes
|
||||||
|
let vNodes = this.keyVNodes.get(key);
|
||||||
|
if (!vNodes) {
|
||||||
|
vNodes = new Set();
|
||||||
|
this.keyVNodes.set(key, vNodes);
|
||||||
|
}
|
||||||
|
vNodes.add(processingVNode);
|
||||||
|
|
||||||
|
// vNode -> keys
|
||||||
|
let keys = this.vNodeKeys.get(processingVNode);
|
||||||
|
if (!keys) {
|
||||||
|
keys = new Set();
|
||||||
|
this.vNodeKeys.set(processingVNode, keys);
|
||||||
|
}
|
||||||
|
keys.add(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
addListener(listener: () => void): void {
|
||||||
|
this.listeners.push(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeListener(listener: () => void): void {
|
||||||
|
this.listeners = this.listeners.filter(item => item != listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
setProp(key: string): void {
|
||||||
|
const vNodes = this.keyVNodes.get(key);
|
||||||
|
vNodes?.forEach((vNode: VNode) => {
|
||||||
|
if (vNode.isStoreChange) {
|
||||||
|
// update already triggered
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
vNode.isStoreChange = true;
|
||||||
|
|
||||||
|
// 触发vNode更新
|
||||||
|
this.triggerUpdate(vNode);
|
||||||
|
});
|
||||||
|
this.triggerChangeListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
triggerChangeListeners(): void {
|
||||||
|
this.listeners.forEach(listener => listener());
|
||||||
|
}
|
||||||
|
|
||||||
|
triggerUpdate(vNode: VNode): void {
|
||||||
|
if (!vNode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
launchUpdateFromVNode(vNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
allChange(): void {
|
||||||
|
let keyIt = this.keyVNodes.keys();
|
||||||
|
let keyItem = keyIt.next();
|
||||||
|
while (!keyItem.done) {
|
||||||
|
this.setProp(keyItem.value);
|
||||||
|
keyItem = keyIt.next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clearByVNode(vNode: VNode): void {
|
||||||
|
const keys = this.vNodeKeys.get(vNode);
|
||||||
|
if (keys) {
|
||||||
|
keys.forEach((key: any) => {
|
||||||
|
const vNodes = this.keyVNodes.get(key);
|
||||||
|
vNodes.delete(vNode);
|
||||||
|
if (vNodes.size === 0) {
|
||||||
|
this.keyVNodes.delete(key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.vNodeKeys.delete(vNode);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
import { createObjectProxy } from './handlers/ObjectProxyHandler';
|
||||||
|
import { Observer } from './Observer';
|
||||||
|
import { HooklessObserver } from './HooklessObserver';
|
||||||
|
import { isArray, isCollection, isObject } from '../CommonUtils';
|
||||||
|
import { createArrayProxy } from './handlers/ArrayProxyHandler';
|
||||||
|
import { createCollectionProxy } from './handlers/CollectionProxyHandler';
|
||||||
|
|
||||||
|
const OBSERVER_KEY = Symbol('_horizonObserver');
|
||||||
|
|
||||||
|
const proxyMap = new WeakMap();
|
||||||
|
|
||||||
|
export const hookObserverMap = new WeakMap();
|
||||||
|
|
||||||
|
export function createProxy(rawObj: any, hookObserver = true): any {
|
||||||
|
// 不是对象(是原始数据类型)不用代理
|
||||||
|
if (!isObject(rawObj)) {
|
||||||
|
return rawObj;
|
||||||
|
}
|
||||||
|
|
||||||
|
const existProxy = proxyMap.get(rawObj);
|
||||||
|
if (existProxy) {
|
||||||
|
return existProxy;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Observer不需要代理
|
||||||
|
if (rawObj instanceof Observer) {
|
||||||
|
return rawObj;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建Observer
|
||||||
|
let observer = getObserver(rawObj);
|
||||||
|
if (!observer) {
|
||||||
|
observer = hookObserver ? new Observer() : new HooklessObserver();
|
||||||
|
rawObj[OBSERVER_KEY] = observer;
|
||||||
|
}
|
||||||
|
|
||||||
|
hookObserverMap.set(rawObj, hookObserver);
|
||||||
|
|
||||||
|
// 创建Proxy
|
||||||
|
let proxyObj;
|
||||||
|
if (isArray(rawObj)) {
|
||||||
|
// 数组
|
||||||
|
proxyObj = createArrayProxy(rawObj as []);
|
||||||
|
} else if (isCollection(rawObj)) {
|
||||||
|
// 集合
|
||||||
|
proxyObj = createCollectionProxy(rawObj);
|
||||||
|
} else {
|
||||||
|
// 原生对象 或 函数
|
||||||
|
proxyObj = createObjectProxy(rawObj);
|
||||||
|
}
|
||||||
|
|
||||||
|
proxyMap.set(rawObj, proxyObj);
|
||||||
|
proxyMap.set(proxyObj, proxyObj);
|
||||||
|
|
||||||
|
return proxyObj;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getObserver(rawObj: any): Observer {
|
||||||
|
return rawObj[OBSERVER_KEY];
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
import { getObserver } from '../ProxyHandler';
|
||||||
|
import { isSame, isValidIntegerKey } from '../../CommonUtils';
|
||||||
|
import { get as objectGet } from './ObjectProxyHandler';
|
||||||
|
|
||||||
|
export function createArrayProxy(rawObj: any[]): any[] {
|
||||||
|
const handle = {
|
||||||
|
get,
|
||||||
|
set,
|
||||||
|
};
|
||||||
|
|
||||||
|
return new Proxy(rawObj, handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
function get(rawObj: any[], key: string, receiver: any) {
|
||||||
|
if (isValidIntegerKey(key) || key === 'length') {
|
||||||
|
return objectGet(rawObj, key, receiver);
|
||||||
|
}
|
||||||
|
return Reflect.get(rawObj, key, receiver);
|
||||||
|
}
|
||||||
|
|
||||||
|
function set(rawObj: any[], key: string, value: any, receiver: any) {
|
||||||
|
const oldValue = rawObj[key];
|
||||||
|
const oldLength = rawObj.length;
|
||||||
|
const newValue = value;
|
||||||
|
|
||||||
|
const ret = Reflect.set(rawObj, key, newValue, receiver);
|
||||||
|
|
||||||
|
const newLength = rawObj.length;
|
||||||
|
const tracker = getObserver(rawObj);
|
||||||
|
|
||||||
|
if (!isSame(newValue, oldValue)) {
|
||||||
|
tracker.setProp(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldLength !== newLength) {
|
||||||
|
tracker.setProp('length');
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
|
@ -0,0 +1,188 @@
|
||||||
|
import { createProxy, getObserver, hookObserverMap } from '../ProxyHandler';
|
||||||
|
import { isMap, isWeakMap, isSame } from '../../CommonUtils';
|
||||||
|
|
||||||
|
const COLLECTION_CHANGE = '_collectionChange';
|
||||||
|
const handler = {
|
||||||
|
get,
|
||||||
|
set,
|
||||||
|
add,
|
||||||
|
delete: deleteFun,
|
||||||
|
clear,
|
||||||
|
has,
|
||||||
|
entries,
|
||||||
|
forEach,
|
||||||
|
keys,
|
||||||
|
values,
|
||||||
|
[Symbol.iterator]: forOf,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createCollectionProxy(rawObj: Object, hookObserver = true): Object {
|
||||||
|
const boundHandler = {};
|
||||||
|
Object.entries(handler).forEach(([id, val]) => {
|
||||||
|
boundHandler[id] = (...args: any[]) => {
|
||||||
|
return (val as any)(...args, hookObserver);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return new Proxy(rawObj, { ...boundHandler });
|
||||||
|
}
|
||||||
|
|
||||||
|
function get(rawObj: { size: number }, key: any, receiver: any): any {
|
||||||
|
if (key === 'size') {
|
||||||
|
return size(rawObj);
|
||||||
|
} else if (key === 'get') {
|
||||||
|
return getFun.bind(null, rawObj);
|
||||||
|
} else if (Object.prototype.hasOwnProperty.call(handler, key)) {
|
||||||
|
const value = Reflect.get(handler, key, receiver);
|
||||||
|
return value.bind(null, rawObj);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Reflect.get(rawObj, key, receiver);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFun(rawObj: { get: (key: any) => any }, key: any) {
|
||||||
|
const tracker = getObserver(rawObj);
|
||||||
|
tracker.useProp(key);
|
||||||
|
|
||||||
|
const value = rawObj.get(key);
|
||||||
|
// 对于value也需要进一步代理
|
||||||
|
const valProxy = createProxy(value, hookObserverMap.get(rawObj));
|
||||||
|
|
||||||
|
return valProxy;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map的set方法
|
||||||
|
function set(
|
||||||
|
rawObj: { get: (key: any) => any; set: (key: any, value: any) => any; has: (key: any) => boolean },
|
||||||
|
key: any,
|
||||||
|
value: any
|
||||||
|
) {
|
||||||
|
const oldValue = rawObj.get(key);
|
||||||
|
const newValue = value;
|
||||||
|
rawObj.set(key, newValue);
|
||||||
|
const valChange = !isSame(newValue, oldValue);
|
||||||
|
const tracker = getObserver(rawObj);
|
||||||
|
|
||||||
|
if (valChange || !rawObj.has(key)) {
|
||||||
|
tracker.setProp(COLLECTION_CHANGE);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (valChange) {
|
||||||
|
tracker.setProp(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
return rawObj;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set的add方法
|
||||||
|
function add(rawObj: { add: (any) => void; set: (string, any) => any; has: (any) => boolean }, value: any): Object {
|
||||||
|
if (!rawObj.has(value)) {
|
||||||
|
rawObj.add(value);
|
||||||
|
|
||||||
|
const tracker = getObserver(rawObj);
|
||||||
|
tracker.setProp(value);
|
||||||
|
tracker.setProp(COLLECTION_CHANGE);
|
||||||
|
}
|
||||||
|
|
||||||
|
return rawObj;
|
||||||
|
}
|
||||||
|
|
||||||
|
function has(rawObj: { has: (string) => boolean }, key: any): boolean {
|
||||||
|
const tracker = getObserver(rawObj);
|
||||||
|
tracker.useProp(key);
|
||||||
|
|
||||||
|
return rawObj.has(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clear(rawObj: { size: number; clear: () => void }) {
|
||||||
|
const oldSize = rawObj.size;
|
||||||
|
rawObj.clear();
|
||||||
|
|
||||||
|
if (oldSize > 0) {
|
||||||
|
const tracker = getObserver(rawObj);
|
||||||
|
tracker.allChange();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteFun(rawObj: { has: (key: any) => boolean; delete: (key: any) => void }, key: any) {
|
||||||
|
if (rawObj.has(key)) {
|
||||||
|
rawObj.delete(key);
|
||||||
|
|
||||||
|
const tracker = getObserver(rawObj);
|
||||||
|
tracker.setProp(key);
|
||||||
|
tracker.setProp(COLLECTION_CHANGE);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function size(rawObj: { size: number }) {
|
||||||
|
const tracker = getObserver(rawObj);
|
||||||
|
tracker.useProp(COLLECTION_CHANGE);
|
||||||
|
return rawObj.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
function keys(rawObj: { keys: () => { next: () => { value: any; done: boolean } } }) {
|
||||||
|
return wrapIterator(rawObj, rawObj.keys());
|
||||||
|
}
|
||||||
|
|
||||||
|
function values(rawObj: { values: () => { next: () => { value: any; done: boolean } } }) {
|
||||||
|
return wrapIterator(rawObj, rawObj.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
function entries(rawObj: { entries: () => { next: () => { value: any; done: boolean } } }) {
|
||||||
|
return wrapIterator(rawObj, rawObj.entries(), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function forOf(rawObj: {
|
||||||
|
entries: () => { next: () => { value: any; done: boolean } };
|
||||||
|
values: () => { next: () => { value: any; done: boolean } };
|
||||||
|
}) {
|
||||||
|
const isMapType = isMap(rawObj) || isWeakMap(rawObj);
|
||||||
|
const iterator = isMapType ? rawObj.entries() : rawObj.values();
|
||||||
|
return wrapIterator(rawObj, iterator, isMapType);
|
||||||
|
}
|
||||||
|
|
||||||
|
function forEach(
|
||||||
|
rawObj: { forEach: (callback: (value: any, key: any) => void) => void },
|
||||||
|
callback: (valProxy: any, keyProxy: any, rawObj: any) => void
|
||||||
|
) {
|
||||||
|
const tracker = getObserver(rawObj);
|
||||||
|
tracker.useProp(COLLECTION_CHANGE);
|
||||||
|
rawObj.forEach((value, key) => {
|
||||||
|
const valProxy = createProxy(value, hookObserverMap.get(rawObj));
|
||||||
|
const keyProxy = createProxy(key, hookObserverMap.get(rawObj));
|
||||||
|
// 最后一个参数要返回代理对象
|
||||||
|
return callback(valProxy, keyProxy, rawObj);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function wrapIterator(rawObj: Object, rawIt: { next: () => { value: any; done: boolean } }, isPair = false) {
|
||||||
|
const tracker = getObserver(rawObj);
|
||||||
|
const hookObserver = hookObserverMap.get(rawObj);
|
||||||
|
tracker.useProp(COLLECTION_CHANGE);
|
||||||
|
|
||||||
|
return {
|
||||||
|
next() {
|
||||||
|
const { value, done } = rawIt.next();
|
||||||
|
if (done) {
|
||||||
|
return { value: createProxy(value, hookObserver), done };
|
||||||
|
}
|
||||||
|
|
||||||
|
tracker.useProp(COLLECTION_CHANGE);
|
||||||
|
|
||||||
|
let newVal;
|
||||||
|
if (isPair) {
|
||||||
|
newVal = [createProxy(value[0], hookObserver), createProxy(value[1], hookObserver)];
|
||||||
|
} else {
|
||||||
|
newVal = createProxy(value, hookObserver);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { value: newVal, done };
|
||||||
|
},
|
||||||
|
[Symbol.iterator]() {
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
import { isSame } from '../../CommonUtils';
|
||||||
|
import { createProxy, getObserver, hookObserverMap } from '../ProxyHandler';
|
||||||
|
|
||||||
|
export function createObjectProxy<T extends object>(rawObj: T): ProxyHandler<T> {
|
||||||
|
const proxy = new Proxy(rawObj, {
|
||||||
|
get,
|
||||||
|
set,
|
||||||
|
});
|
||||||
|
|
||||||
|
return proxy;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function get(rawObj: object, key: string, receiver: any): any {
|
||||||
|
const observer = getObserver(rawObj);
|
||||||
|
|
||||||
|
if (key === 'addListener') {
|
||||||
|
return observer.addListener.bind(observer);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === 'removeListener') {
|
||||||
|
return observer.removeListener.bind(observer);
|
||||||
|
}
|
||||||
|
|
||||||
|
observer.useProp(key);
|
||||||
|
|
||||||
|
const value = Reflect.get(rawObj, key, receiver);
|
||||||
|
|
||||||
|
// 对于value也需要进一步代理
|
||||||
|
const valProxy = createProxy(value, hookObserverMap.get(rawObj));
|
||||||
|
|
||||||
|
return valProxy;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function set(rawObj: object, key: string, value: any, receiver: any): boolean {
|
||||||
|
const observer = getObserver(rawObj);
|
||||||
|
|
||||||
|
if (value && key == 'removeListener') {
|
||||||
|
observer.removeListener(value);
|
||||||
|
}
|
||||||
|
const oldValue = rawObj[key];
|
||||||
|
const newValue = value;
|
||||||
|
|
||||||
|
const ret = Reflect.set(rawObj, key, newValue, receiver);
|
||||||
|
|
||||||
|
if (!isSame(newValue, oldValue)) {
|
||||||
|
observer.setProp(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { isObject } from '../CommonUtils';
|
||||||
|
|
||||||
|
export function readonlyProxy<T extends object>(target: T): ProxyHandler<T> {
|
||||||
|
return new Proxy(target, {
|
||||||
|
get(target, property, receiver) {
|
||||||
|
const result = Reflect.get(target, property, receiver);
|
||||||
|
try {
|
||||||
|
if (isObject(result)) {
|
||||||
|
return readonlyProxy(result);
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
|
||||||
|
set() {
|
||||||
|
throw Error('Trying to change readonly variable');
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteProperty() {
|
||||||
|
throw Error('Trying to change readonly variable');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default readonlyProxy;
|
|
@ -0,0 +1,225 @@
|
||||||
|
//@ts-ignore
|
||||||
|
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 } from '../types';
|
||||||
|
import { Observer } from '../proxy/Observer';
|
||||||
|
import { FunctionComponent, ClassComponent } from '../Constants';
|
||||||
|
|
||||||
|
const storeMap = new Map();
|
||||||
|
|
||||||
|
function isPromise(obj: any): boolean {
|
||||||
|
return !!obj && (typeof obj === 'object' || typeof obj === 'function') && typeof obj.then === 'function';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createStore<S extends object, A extends UserActions<S>, C extends UserComputedValues<S>>(
|
||||||
|
config: StoreConfig<S, A, C>
|
||||||
|
): () => StoreHandler<S, A, C> {
|
||||||
|
let handler: any = {
|
||||||
|
$subscribe: null,
|
||||||
|
$unsubscribe: null,
|
||||||
|
$state: null,
|
||||||
|
$config: config,
|
||||||
|
$queue: null,
|
||||||
|
$actions: {},
|
||||||
|
$computed: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const obj = {
|
||||||
|
...config,
|
||||||
|
config,
|
||||||
|
plannedActions: [],
|
||||||
|
rawState: config.state,
|
||||||
|
rawActions: { ...config.actions },
|
||||||
|
};
|
||||||
|
|
||||||
|
// 校验
|
||||||
|
if (Object.prototype.toString.call(obj) !== '[object Object]') {
|
||||||
|
throw new Error('store obj must be pure object');
|
||||||
|
}
|
||||||
|
|
||||||
|
const proxyObj = createProxy(obj.state, !obj.options?.suppressHooks);
|
||||||
|
proxyObj.$pending = false;
|
||||||
|
handler.$subscribe = listener => {
|
||||||
|
proxyObj.addListener(listener);
|
||||||
|
};
|
||||||
|
handler.$unsubscribe = listener => {
|
||||||
|
proxyObj.removeListener(listener);
|
||||||
|
};
|
||||||
|
obj.rawState = obj.state;
|
||||||
|
obj.state = proxyObj;
|
||||||
|
|
||||||
|
handler.$state = obj.state;
|
||||||
|
|
||||||
|
handler.$config = obj.config;
|
||||||
|
|
||||||
|
// handles.$reset = ()=>{
|
||||||
|
// const keys = Object.keys(obj.state);
|
||||||
|
// Object.entries(obj.defaultState).forEach(([key,value])=>{
|
||||||
|
// obj.state[key]=value;
|
||||||
|
// });
|
||||||
|
// keys.forEach(key => {
|
||||||
|
// if(!obj.defaultState[key]){
|
||||||
|
// delete obj.state[key];
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// };
|
||||||
|
|
||||||
|
function tryNextAction() {
|
||||||
|
if (!obj.plannedActions.length) {
|
||||||
|
proxyObj.$pending = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextAction = obj.plannedActions.shift();
|
||||||
|
const result = obj.rawActions[nextAction.action].bind(self, obj.state)(...nextAction.payload);
|
||||||
|
|
||||||
|
if (isPromise(result)) {
|
||||||
|
result.then(value => {
|
||||||
|
nextAction.resolve(value);
|
||||||
|
tryNextAction();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
nextAction.resolve(result);
|
||||||
|
tryNextAction();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 包装actions
|
||||||
|
Object.keys(obj.actions).forEach(key => {
|
||||||
|
(obj.actions as any)[key] = handler[key] = function Wrapped(...payload) {
|
||||||
|
return obj.rawActions[key].bind(self, obj.state)(...payload);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
handler.$queue = {};
|
||||||
|
Object.keys(obj.rawActions).forEach(action => {
|
||||||
|
handler.$queue[action] = (...payload) => {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
if (!proxyObj.$pending) {
|
||||||
|
proxyObj.$pending = true;
|
||||||
|
const result = obj.rawActions[action].bind(self, obj.state)(...payload);
|
||||||
|
|
||||||
|
if (isPromise(result)) {
|
||||||
|
result.then(value => {
|
||||||
|
resolve(value);
|
||||||
|
tryNextAction();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
resolve(result);
|
||||||
|
tryNextAction();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
obj.plannedActions.push({
|
||||||
|
action,
|
||||||
|
payload,
|
||||||
|
resolve,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
handler.$actions = obj.actions;
|
||||||
|
|
||||||
|
// native getters
|
||||||
|
Object.keys(obj.state).forEach(key => {
|
||||||
|
Object.defineProperty(handler, key, {
|
||||||
|
get: () => obj.state[key],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// computed
|
||||||
|
if (obj.computed) {
|
||||||
|
Object.keys(obj.computed).forEach(key => {
|
||||||
|
// supports access through attributes
|
||||||
|
Object.defineProperty(handler, key, {
|
||||||
|
get: obj.computed[key].bind(handler, readonlyProxy(obj.state)),
|
||||||
|
});
|
||||||
|
|
||||||
|
// supports access through function
|
||||||
|
(obj.computed as any)[key] = obj.computed[key].bind(handler, readonlyProxy(obj.state));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
handler.$computed = obj.computed || {};
|
||||||
|
|
||||||
|
if (config.id) {
|
||||||
|
storeMap.set(config.id, handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
return createStoreHook(handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearVNodeObservers(vNode) {
|
||||||
|
vNode.observers.forEach(observer => {
|
||||||
|
observer.clearByVNode(vNode);
|
||||||
|
});
|
||||||
|
|
||||||
|
vNode.observers.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
function hookStore() {
|
||||||
|
const processingVNode = getProcessingVNode();
|
||||||
|
|
||||||
|
// did not execute in a component
|
||||||
|
if (!processingVNode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (processingVNode.observers) {
|
||||||
|
// 清除上一次缓存的Observer依赖
|
||||||
|
clearVNodeObservers(processingVNode);
|
||||||
|
} else {
|
||||||
|
processingVNode.observers = new Set<Observer>();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (processingVNode.tag === FunctionComponent) {
|
||||||
|
// from FunctionComponent
|
||||||
|
const vNodeRef = useRef(null);
|
||||||
|
vNodeRef.current = processingVNode;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
clearVNodeObservers(vNodeRef.current);
|
||||||
|
vNodeRef.current.observers = null;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
} else if (processingVNode.tag === ClassComponent) {
|
||||||
|
// from ClassComponent
|
||||||
|
if (!processingVNode.classComponentWillUnmount) {
|
||||||
|
processingVNode.classComponentWillUnmount = function(vNode) {
|
||||||
|
clearVNodeObservers(vNode);
|
||||||
|
vNode.observers = null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createStoreHook<S extends object, A extends UserActions<S>, C extends UserComputedValues<S>>(
|
||||||
|
storeHandler: StoreHandler<S, A, C>
|
||||||
|
): () => StoreHandler<S, A, C> {
|
||||||
|
return () => {
|
||||||
|
if (!storeHandler.$config.options?.suppressHooks) {
|
||||||
|
hookStore();
|
||||||
|
}
|
||||||
|
|
||||||
|
return storeHandler;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useStore<S extends object, A extends UserActions<S>, C extends UserComputedValues<S>>(
|
||||||
|
id: string
|
||||||
|
): StoreHandler<S, A, C> {
|
||||||
|
const storeObj = storeMap.get(id);
|
||||||
|
|
||||||
|
if (!storeObj.$config.options?.suppressHooks) {
|
||||||
|
hookStore();
|
||||||
|
}
|
||||||
|
|
||||||
|
return storeObj;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearStore(id: string): void {
|
||||||
|
storeMap.delete(id);
|
||||||
|
}
|
|
@ -0,0 +1,75 @@
|
||||||
|
export interface IObserver {
|
||||||
|
vNodeKeys: WeakMap<any, any>;
|
||||||
|
|
||||||
|
keyVNodes: Map<any, any>;
|
||||||
|
|
||||||
|
listeners: (() => void)[];
|
||||||
|
|
||||||
|
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 UserActions<S extends object> = { [K: string]: StateFunction<S> };
|
||||||
|
type UserComputedValues<S extends object> = { [K: string]: StateFunction<S> };
|
||||||
|
|
||||||
|
type StateFunction<S extends object> = (state: S, ...args: any[]) => any;
|
||||||
|
type StoreActions<S extends object, A extends UserActions<S>> = { [K in keyof A]: A[K] };
|
||||||
|
type ComputedValues<S extends object, C extends UserComputedValues<S>> = { [K in keyof C]: C[K] };
|
||||||
|
type PostponedAction = (state: object, ...args: any[]) => Promise<any>;
|
||||||
|
type PostponedActions = { [key: string]: PostponedAction };
|
||||||
|
|
||||||
|
export type StoreHandler<S extends object, A extends UserActions<S>, C extends UserComputedValues<S>> = {
|
||||||
|
$subscribe: (listener: () => void) => void;
|
||||||
|
$unsubscribe: (listener: () => void) => void;
|
||||||
|
$state: S;
|
||||||
|
$config: StoreConfig<S, A, C>;
|
||||||
|
$queue: StoreActions<S, A>;
|
||||||
|
$actions: StoreActions<S, A>;
|
||||||
|
$computed: StoreActions<S, A>;
|
||||||
|
reduxHandler?: ReduxStoreHandler;
|
||||||
|
} & { [K in keyof S]: S[K] } &
|
||||||
|
{ [K in keyof A]: A[K] } &
|
||||||
|
{ [K in keyof C]: C[K] };
|
||||||
|
|
||||||
|
export type StoreConfig<S extends object, A extends UserActions<S>, C extends UserComputedValues<S>> = {
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
|
@ -0,0 +1,201 @@
|
||||||
|
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 (
|
||||||
|
<div>
|
||||||
|
<button id={'addBtn'} onClick={addOnePerson}>
|
||||||
|
add person
|
||||||
|
</button>
|
||||||
|
<button id={'delBtn'} onClick={delOnePerson}>
|
||||||
|
delete person
|
||||||
|
</button>
|
||||||
|
<div>{props.children}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
it('测试Array方法: push()、pop()', () => {
|
||||||
|
function Child(props) {
|
||||||
|
const userStore = useStore('user');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Text id={'hasPerson'} text={`has new person: ${userStore.$state.persons.length}`} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Horizon.render(<App parent={Parent} child={Child} />, 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 (
|
||||||
|
<div>
|
||||||
|
<Text id={'nameList'} text={`name list: ${nameList.join(' ')}`} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Horizon.render(<App parent={Parent} child={Child} />, 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 (
|
||||||
|
<div>
|
||||||
|
<Text id={'nameList'} text={`name list: ${nameList.join(' ')}`} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Horizon.render(<App parent={Parent} child={Child} />, 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');
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,323 @@
|
||||||
|
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中的Map', () => {
|
||||||
|
const { unmountComponentAtNode } = Horizon;
|
||||||
|
let container = 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();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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(newPerson);
|
||||||
|
};
|
||||||
|
const clearPersons = function() {
|
||||||
|
userStore.clearPersons();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<button id={'addBtn'} onClick={addOnePerson}>
|
||||||
|
add person
|
||||||
|
</button>
|
||||||
|
<button id={'delBtn'} onClick={delOnePerson}>
|
||||||
|
delete person
|
||||||
|
</button>
|
||||||
|
<button id={'clearBtn'} onClick={clearPersons}>
|
||||||
|
clear persons
|
||||||
|
</button>
|
||||||
|
<div>{props.children}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
it('测试Map方法: set()、delete()、clear()', () => {
|
||||||
|
function Child(props) {
|
||||||
|
const userStore = useStore('user');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Text id={'size'} text={`persons number: ${userStore.$state.persons.size}`} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Horizon.render(<App parent={Parent} child={Child} />, container);
|
||||||
|
|
||||||
|
expect(container.querySelector('#size').innerHTML).toBe('persons number: 2');
|
||||||
|
// 在Map中增加一个对象
|
||||||
|
Horizon.act(() => {
|
||||||
|
triggerClickEvent(container, 'addBtn');
|
||||||
|
});
|
||||||
|
expect(container.querySelector('#size').innerHTML).toBe('persons number: 3');
|
||||||
|
|
||||||
|
// 在Map中删除一个对象
|
||||||
|
Horizon.act(() => {
|
||||||
|
triggerClickEvent(container, 'delBtn');
|
||||||
|
});
|
||||||
|
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');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('测试Map方法: keys()', () => {
|
||||||
|
function Child(props) {
|
||||||
|
const userStore = useStore('user');
|
||||||
|
|
||||||
|
const nameList = [];
|
||||||
|
const keys = userStore.$state.persons.keys();
|
||||||
|
for (const key of keys) {
|
||||||
|
nameList.push(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Text id={'nameList'} text={`name list: ${nameList.join(' ')}`} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Horizon.render(<App parent={Parent} child={Child} />, container);
|
||||||
|
|
||||||
|
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');
|
||||||
|
|
||||||
|
// 在Map中删除一个对象
|
||||||
|
Horizon.act(() => {
|
||||||
|
triggerClickEvent(container, 'delBtn');
|
||||||
|
});
|
||||||
|
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: ');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('测试Map方法: values()', () => {
|
||||||
|
function Child(props) {
|
||||||
|
const userStore = useStore('user');
|
||||||
|
|
||||||
|
const ageList = [];
|
||||||
|
const values = userStore.$state.persons.values();
|
||||||
|
for (const val of values) {
|
||||||
|
ageList.push(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Text id={'ageList'} text={`age list: ${ageList.join(' ')}`} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Horizon.render(<App parent={Parent} child={Child} />, container);
|
||||||
|
|
||||||
|
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');
|
||||||
|
|
||||||
|
// 在Map中删除一个对象
|
||||||
|
Horizon.act(() => {
|
||||||
|
triggerClickEvent(container, 'delBtn');
|
||||||
|
});
|
||||||
|
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: ');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('测试Map方法: entries()', () => {
|
||||||
|
function Child(props) {
|
||||||
|
const userStore = useStore('user');
|
||||||
|
|
||||||
|
const nameList = [];
|
||||||
|
const entries = userStore.$state.persons.entries();
|
||||||
|
for (const entry of entries) {
|
||||||
|
nameList.push(entry[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Text id={'nameList'} text={`name list: ${nameList.join(' ')}`} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Horizon.render(<App parent={Parent} child={Child} />, container);
|
||||||
|
|
||||||
|
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');
|
||||||
|
|
||||||
|
// 在Map中删除一个对象
|
||||||
|
Horizon.act(() => {
|
||||||
|
triggerClickEvent(container, 'delBtn');
|
||||||
|
});
|
||||||
|
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: ');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('测试Map方法: forEach()', () => {
|
||||||
|
function Child(props) {
|
||||||
|
const userStore = useStore('user');
|
||||||
|
|
||||||
|
const nameList = [];
|
||||||
|
userStore.$state.persons.forEach((val, key) => {
|
||||||
|
nameList.push(key);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Text id={'nameList'} text={`name list: ${nameList.join(' ')}`} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Horizon.render(<App parent={Parent} child={Child} />, container);
|
||||||
|
|
||||||
|
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');
|
||||||
|
|
||||||
|
// 在Map中删除一个对象
|
||||||
|
Horizon.act(() => {
|
||||||
|
triggerClickEvent(container, 'delBtn');
|
||||||
|
});
|
||||||
|
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: ');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('测试Map方法: has()', () => {
|
||||||
|
function Child(props) {
|
||||||
|
const userStore = useStore('user');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Text id={'hasPerson'} text={`has new person: ${userStore.$state.persons.has(newPerson.name)}`} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Horizon.render(<App parent={Parent} child={Child} />, container);
|
||||||
|
|
||||||
|
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');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('测试Map方法: for of()', () => {
|
||||||
|
function Child(props) {
|
||||||
|
const userStore = useStore('user');
|
||||||
|
|
||||||
|
const nameList = [];
|
||||||
|
for (const per of userStore.$state.persons) {
|
||||||
|
nameList.push(per[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Text id={'nameList'} text={`name list: ${nameList.join(' ')}`} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Horizon.render(<App parent={Parent} child={Child} />, container);
|
||||||
|
|
||||||
|
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');
|
||||||
|
|
||||||
|
// 在Map中删除一个对象
|
||||||
|
Horizon.act(() => {
|
||||||
|
triggerClickEvent(container, 'delBtn');
|
||||||
|
});
|
||||||
|
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: ');
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,164 @@
|
||||||
|
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中的混合类型变化', () => {
|
||||||
|
const { unmountComponentAtNode } = Horizon;
|
||||||
|
let container = null;
|
||||||
|
beforeEach(() => {
|
||||||
|
// 创建一个 DOM 元素作为渲染目标
|
||||||
|
container = document.createElement('div');
|
||||||
|
document.body.appendChild(container);
|
||||||
|
|
||||||
|
const persons = new Set([{ name: 'p1', age: 1, love: new Map() }]);
|
||||||
|
persons.add({
|
||||||
|
name: 'p2',
|
||||||
|
age: 2,
|
||||||
|
love: new Map(),
|
||||||
|
});
|
||||||
|
persons
|
||||||
|
.values()
|
||||||
|
.next()
|
||||||
|
.value.love.set('lanqiu', { moneny: 100, days: [1, 3, 5] });
|
||||||
|
|
||||||
|
createStore({
|
||||||
|
id: 'user',
|
||||||
|
state: {
|
||||||
|
type: 'bing dun dun',
|
||||||
|
persons: persons,
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
addDay: (state, day) => {
|
||||||
|
state.persons
|
||||||
|
.values()
|
||||||
|
.next()
|
||||||
|
.value.love.get('lanqiu')
|
||||||
|
.days.push(day);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// 退出时进行清理
|
||||||
|
unmountComponentAtNode(container);
|
||||||
|
container.remove();
|
||||||
|
container = null;
|
||||||
|
|
||||||
|
clearStore('user');
|
||||||
|
});
|
||||||
|
|
||||||
|
function Parent(props) {
|
||||||
|
const userStore = useStore('user');
|
||||||
|
const addDay = function() {
|
||||||
|
userStore.addDay(7);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<button id={'addBtn'} onClick={addDay}>
|
||||||
|
add day
|
||||||
|
</button>
|
||||||
|
<div>{props.children}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
it('测试state -> set -> map -> array的数据变化', () => {
|
||||||
|
function Child(props) {
|
||||||
|
const userStore = useStore('user');
|
||||||
|
|
||||||
|
const days = userStore.$state.persons
|
||||||
|
.values()
|
||||||
|
.next()
|
||||||
|
.value.love.get('lanqiu').days;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Text id={'dayList'} text={`love: ${days.join(' ')}`} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Horizon.render(<App parent={Parent} child={Child} />, container);
|
||||||
|
|
||||||
|
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');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('属性是个class实例', () => {
|
||||||
|
class Person {
|
||||||
|
name;
|
||||||
|
age;
|
||||||
|
loves = new Set();
|
||||||
|
|
||||||
|
constructor(name, age) {
|
||||||
|
this.name = name;
|
||||||
|
this.age = age;
|
||||||
|
}
|
||||||
|
|
||||||
|
setName(name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
getName() {
|
||||||
|
return this.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
setAge(age) {
|
||||||
|
this.age = age;
|
||||||
|
}
|
||||||
|
|
||||||
|
getAge() {
|
||||||
|
return this.age;
|
||||||
|
}
|
||||||
|
|
||||||
|
addLove(lv) {
|
||||||
|
this.loves.add(lv);
|
||||||
|
}
|
||||||
|
|
||||||
|
getLoves() {
|
||||||
|
return this.loves;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
while (!per.done) {
|
||||||
|
nameList.push(per.value.name ?? per.value.getName());
|
||||||
|
globalPerson = per.value;
|
||||||
|
per = valIterator.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Text id={'nameList'} text={nameList.join(' ')} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Horizon.render(<App parent={Parent} child={Child} />, container);
|
||||||
|
|
||||||
|
expect(container.querySelector('#nameList').innerHTML).toBe('p1 p2');
|
||||||
|
|
||||||
|
// 动态增加一个Person实例
|
||||||
|
globalStore.$state.persons.add(new Person('ClassPerson', 5));
|
||||||
|
|
||||||
|
expect(container.querySelector('#nameList').innerHTML).toBe('p1 p2 ClassPerson');
|
||||||
|
|
||||||
|
globalPerson.setName('ClassPerson1');
|
||||||
|
|
||||||
|
expect(container.querySelector('#nameList').innerHTML).toBe('p1 p2 ClassPerson1');
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,294 @@
|
||||||
|
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中的Set', () => {
|
||||||
|
const { unmountComponentAtNode } = Horizon;
|
||||||
|
let container = 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();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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(newPerson);
|
||||||
|
};
|
||||||
|
const clearPersons = function() {
|
||||||
|
userStore.clearPersons();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<button id={'addBtn'} onClick={addOnePerson}>
|
||||||
|
add person
|
||||||
|
</button>
|
||||||
|
<button id={'delBtn'} onClick={delOnePerson}>
|
||||||
|
delete person
|
||||||
|
</button>
|
||||||
|
<button id={'clearBtn'} onClick={clearPersons}>
|
||||||
|
clear persons
|
||||||
|
</button>
|
||||||
|
<div>{props.children}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
for (const key of keys) {
|
||||||
|
nameList.push(key.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Text id={'size'} text={`persons number: ${userStore.$state.persons.size}`} />
|
||||||
|
<Text id={'lastAge'} text={`last person age: ${personArr[personArr.length - 1]?.age ?? 0}`} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Horizon.render(<App parent={Parent} child={Child} />, container);
|
||||||
|
|
||||||
|
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');
|
||||||
|
|
||||||
|
// 在set中删除一个对象
|
||||||
|
Horizon.act(() => {
|
||||||
|
triggerClickEvent(container, 'delBtn');
|
||||||
|
});
|
||||||
|
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');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('测试Set方法: keys()、values()', () => {
|
||||||
|
function Child(props) {
|
||||||
|
const userStore = useStore('user');
|
||||||
|
|
||||||
|
const nameList = [];
|
||||||
|
const keys = userStore.$state.persons.keys();
|
||||||
|
// const keys = userStore.$state.persons.values();
|
||||||
|
for (const key of keys) {
|
||||||
|
nameList.push(key.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Text id={'nameList'} text={`name list: ${nameList.join(' ')}`} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Horizon.render(<App parent={Parent} child={Child} />, container);
|
||||||
|
|
||||||
|
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');
|
||||||
|
|
||||||
|
// 在set中删除一个对象
|
||||||
|
Horizon.act(() => {
|
||||||
|
triggerClickEvent(container, 'delBtn');
|
||||||
|
});
|
||||||
|
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: ');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('测试Set方法: entries()', () => {
|
||||||
|
function Child(props) {
|
||||||
|
const userStore = useStore('user');
|
||||||
|
|
||||||
|
const nameList = [];
|
||||||
|
const entries = userStore.$state.persons.entries();
|
||||||
|
for (const entry of entries) {
|
||||||
|
nameList.push(entry[0].name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Text id={'nameList'} text={`name list: ${nameList.join(' ')}`} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Horizon.render(<App parent={Parent} child={Child} />, container);
|
||||||
|
|
||||||
|
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');
|
||||||
|
|
||||||
|
// 在set中删除一个对象
|
||||||
|
Horizon.act(() => {
|
||||||
|
triggerClickEvent(container, 'delBtn');
|
||||||
|
});
|
||||||
|
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: ');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('测试Set方法: forEach()', () => {
|
||||||
|
function Child(props) {
|
||||||
|
const userStore = useStore('user');
|
||||||
|
|
||||||
|
const nameList = [];
|
||||||
|
userStore.$state.persons.forEach(per => {
|
||||||
|
nameList.push(per.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Text id={'nameList'} text={`name list: ${nameList.join(' ')}`} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Horizon.render(<App parent={Parent} child={Child} />, container);
|
||||||
|
|
||||||
|
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');
|
||||||
|
|
||||||
|
// 在set中删除一个对象
|
||||||
|
Horizon.act(() => {
|
||||||
|
triggerClickEvent(container, 'delBtn');
|
||||||
|
});
|
||||||
|
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: ');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('测试Set方法: has()', () => {
|
||||||
|
function Child(props) {
|
||||||
|
const userStore = useStore('user');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Text id={'hasPerson'} text={`has new person: ${userStore.$state.persons.has(newPerson)}`} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Horizon.render(<App parent={Parent} child={Child} />, container);
|
||||||
|
|
||||||
|
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');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('测试Set方法: for of()', () => {
|
||||||
|
function Child(props) {
|
||||||
|
const userStore = useStore('user');
|
||||||
|
|
||||||
|
const nameList = [];
|
||||||
|
for (const per of userStore.$state.persons) {
|
||||||
|
nameList.push(per.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Text id={'nameList'} text={`name list: ${nameList.join(' ')}`} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Horizon.render(<App parent={Parent} child={Child} />, container);
|
||||||
|
|
||||||
|
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');
|
||||||
|
|
||||||
|
// 在set中删除一个对象
|
||||||
|
Horizon.act(() => {
|
||||||
|
triggerClickEvent(container, 'delBtn');
|
||||||
|
});
|
||||||
|
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: ');
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,124 @@
|
||||||
|
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中的WeakMap', () => {
|
||||||
|
const { unmountComponentAtNode } = Horizon;
|
||||||
|
let container = 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();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// 退出时进行清理
|
||||||
|
unmountComponentAtNode(container);
|
||||||
|
container.remove();
|
||||||
|
container = null;
|
||||||
|
|
||||||
|
clearStore('user');
|
||||||
|
});
|
||||||
|
|
||||||
|
const newPerson = { name: 'p3' };
|
||||||
|
|
||||||
|
function Parent(props) {
|
||||||
|
const userStore = useStore('user');
|
||||||
|
const addOnePerson = function() {
|
||||||
|
userStore.addOnePerson(newPerson);
|
||||||
|
};
|
||||||
|
const delOnePerson = function() {
|
||||||
|
userStore.delOnePerson(newPerson);
|
||||||
|
};
|
||||||
|
const clearPersons = function() {
|
||||||
|
userStore.clearPersons();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<button id={'addBtn'} onClick={addOnePerson}>
|
||||||
|
add person
|
||||||
|
</button>
|
||||||
|
<button id={'delBtn'} onClick={delOnePerson}>
|
||||||
|
delete person
|
||||||
|
</button>
|
||||||
|
<button id={'clearBtn'} onClick={clearPersons}>
|
||||||
|
clear persons
|
||||||
|
</button>
|
||||||
|
<div>{props.children}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
it('测试WeakMap方法: set()、delete()、has()', () => {
|
||||||
|
function Child(props) {
|
||||||
|
const userStore = useStore('user');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Text id={'hasPerson'} text={`has new person: ${userStore.$state.persons.has(newPerson)}`} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Horizon.render(<App parent={Parent} child={Child} />, container);
|
||||||
|
|
||||||
|
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');
|
||||||
|
|
||||||
|
// 在WeakMap中删除一个对象
|
||||||
|
Horizon.act(() => {
|
||||||
|
triggerClickEvent(container, 'delBtn');
|
||||||
|
});
|
||||||
|
expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: false');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('测试WeakMap方法: get()', () => {
|
||||||
|
function Child(props) {
|
||||||
|
const userStore = useStore('user');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Text id={'hasPerson'} text={`has new person: ${userStore.$state.persons.get(newPerson)}`} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Horizon.render(<App parent={Parent} child={Child} />, container);
|
||||||
|
|
||||||
|
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');
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,96 @@
|
||||||
|
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中的WeakSet', () => {
|
||||||
|
const { unmountComponentAtNode } = Horizon;
|
||||||
|
let container = 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();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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(newPerson);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<button id={'addBtn'} onClick={addOnePerson}>
|
||||||
|
add person
|
||||||
|
</button>
|
||||||
|
<button id={'delBtn'} onClick={delOnePerson}>
|
||||||
|
delete person
|
||||||
|
</button>
|
||||||
|
<div>{props.children}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
it('测试WeakSet方法: add()、delete()、has()', () => {
|
||||||
|
function Child(props) {
|
||||||
|
const userStore = useStore('user');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Text id={'hasPerson'} text={`has new person: ${userStore.$state.persons.has(newPerson)}`} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Horizon.render(<App parent={Parent} child={Child} />, container);
|
||||||
|
|
||||||
|
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');
|
||||||
|
|
||||||
|
// 在WeakSet中删除一个对象
|
||||||
|
Horizon.act(() => {
|
||||||
|
triggerClickEvent(container, 'delBtn');
|
||||||
|
});
|
||||||
|
expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: false');
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,161 @@
|
||||||
|
import * as Horizon from '@cloudsop/horizon/index.ts';
|
||||||
|
import { createStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler';
|
||||||
|
import { triggerClickEvent } from '../../jest/commonComponents';
|
||||||
|
|
||||||
|
const { unmountComponentAtNode } = Horizon;
|
||||||
|
|
||||||
|
function postpone(timer, func) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
setTimeout(function() {
|
||||||
|
resolve(func());
|
||||||
|
}, timer);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Asynchronous functions', () => {
|
||||||
|
let container = null;
|
||||||
|
|
||||||
|
const COUNTER_ID = 'counter';
|
||||||
|
const TOGGLE_ID = 'toggle';
|
||||||
|
const TOGGLE_FAST_ID = 'toggleFast';
|
||||||
|
const RESULT_ID = 'result';
|
||||||
|
|
||||||
|
let useAsyncCounter;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
useAsyncCounter = createStore({
|
||||||
|
state: {
|
||||||
|
counter: 0,
|
||||||
|
check: false,
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
increment: function(state) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
setTimeout(() => {
|
||||||
|
state.counter++;
|
||||||
|
resolve();
|
||||||
|
}, 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);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
unmountComponentAtNode(container);
|
||||||
|
container.remove();
|
||||||
|
container = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should wait for async actions', async () => {
|
||||||
|
jest.useRealTimers();
|
||||||
|
let globalStore;
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const store = useAsyncCounter();
|
||||||
|
globalStore = store;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p id={RESULT_ID}>{store.value}</p>
|
||||||
|
<button onClick={store.$queue.increment} id={COUNTER_ID}>
|
||||||
|
add 1
|
||||||
|
</button>
|
||||||
|
<button onClick={store.$queue.toggle} id={TOGGLE_ID}>
|
||||||
|
slow toggle
|
||||||
|
</button>
|
||||||
|
<button onClick={store.toggle} id={TOGGLE_FAST_ID}>
|
||||||
|
fast toggle
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Horizon.render(<App />, 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 () => {
|
||||||
|
jest.useFakeTimers();
|
||||||
|
let globalStore;
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const store = useAsyncCounter();
|
||||||
|
globalStore = store;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p id={RESULT_ID}>{store.value}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Horizon.render(<App />, container);
|
||||||
|
|
||||||
|
// call async action by then
|
||||||
|
globalStore.$queue.increment().then(() => {
|
||||||
|
expect(document.getElementById(RESULT_ID).innerHTML).toBe('false1');
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(document.getElementById(RESULT_ID).innerHTML).toBe('false0');
|
||||||
|
|
||||||
|
// past 150 ms
|
||||||
|
jest.advanceTimersByTime(150);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,63 @@
|
||||||
|
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 <div id={RESULT_ID}>{logStore.length}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
Horizon.render(<App />, container);
|
||||||
|
|
||||||
|
expect(document.getElementById(RESULT_ID).innerHTML).toBe('1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should use actions and update components', () => {
|
||||||
|
function App() {
|
||||||
|
const logStore = useLogStore();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
id={BUTTON_ID}
|
||||||
|
onClick={() => {
|
||||||
|
logStore.addLog('a');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
add
|
||||||
|
</button>
|
||||||
|
<p id={RESULT_ID}>{logStore.length}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Horizon.render(<App />, container);
|
||||||
|
|
||||||
|
Horizon.act(() => {
|
||||||
|
triggerClickEvent(container, BUTTON_ID);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(document.getElementById(RESULT_ID).innerHTML).toBe('2');
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,63 @@
|
||||||
|
import * as Horizon from '@cloudsop/horizon/index.ts';
|
||||||
|
import { triggerClickEvent } from '../../jest/commonComponents';
|
||||||
|
import { useLogStore } from './store';
|
||||||
|
|
||||||
|
const { unmountComponentAtNode } = Horizon;
|
||||||
|
|
||||||
|
describe('Dollar store access', () => {
|
||||||
|
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 $state and $computed', () => {
|
||||||
|
function App() {
|
||||||
|
const logStore = useLogStore();
|
||||||
|
|
||||||
|
return <div id={RESULT_ID}>{logStore.$computed.length()}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
Horizon.render(<App />, container);
|
||||||
|
|
||||||
|
expect(document.getElementById(RESULT_ID).innerHTML).toBe('1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should use $actions and update components', () => {
|
||||||
|
function App() {
|
||||||
|
const logStore = useLogStore();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
id={BUTTON_ID}
|
||||||
|
onClick={() => {
|
||||||
|
logStore.$actions.addLog();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
add
|
||||||
|
</button>
|
||||||
|
<p id={RESULT_ID}>{logStore.$computed.length()}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Horizon.render(<App />, container);
|
||||||
|
|
||||||
|
Horizon.act(() => {
|
||||||
|
triggerClickEvent(container, BUTTON_ID);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(document.getElementById(RESULT_ID).innerHTML).toBe('2');
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,148 @@
|
||||||
|
import * as Horizon from '@cloudsop/horizon/index.ts';
|
||||||
|
import { createStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler';
|
||||||
|
import { triggerClickEvent } from '../../jest/commonComponents';
|
||||||
|
|
||||||
|
const { unmountComponentAtNode } = Horizon;
|
||||||
|
|
||||||
|
describe('Self referencing', () => {
|
||||||
|
let container = null;
|
||||||
|
|
||||||
|
const BUTTON_ID = 'btn';
|
||||||
|
const RESULT_ID = 'result';
|
||||||
|
|
||||||
|
const useSelfRefStore = createStore({
|
||||||
|
state: {
|
||||||
|
val: 2,
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
magic: function(state) {
|
||||||
|
state.val = state.val * 2 - 1;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
value: state => state.val,
|
||||||
|
double: function() {
|
||||||
|
return this.value * 2;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
container = document.createElement('div');
|
||||||
|
document.body.appendChild(container);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
unmountComponentAtNode(container);
|
||||||
|
container.remove();
|
||||||
|
container = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should use own getters', () => {
|
||||||
|
function App() {
|
||||||
|
const store = useSelfRefStore();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p id={RESULT_ID}>{store.double}</p>
|
||||||
|
<button onClick={store.magic} id={BUTTON_ID}>
|
||||||
|
do magic
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Horizon.render(<App />, container);
|
||||||
|
|
||||||
|
expect(document.getElementById(RESULT_ID).innerHTML).toBe('4');
|
||||||
|
|
||||||
|
Horizon.act(() => {
|
||||||
|
triggerClickEvent(container, BUTTON_ID);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(document.getElementById(RESULT_ID).innerHTML).toBe('6');
|
||||||
|
|
||||||
|
Horizon.act(() => {
|
||||||
|
triggerClickEvent(container, BUTTON_ID);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(document.getElementById(RESULT_ID).innerHTML).toBe('10');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should access other stores', () => {
|
||||||
|
const useOtherStore = createStore({
|
||||||
|
state: {},
|
||||||
|
actions: {
|
||||||
|
doMagic: () => useSelfRefStore().magic(),
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
magicConstant: () => useSelfRefStore().value,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const store = useOtherStore();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p id={RESULT_ID}>{store.magicConstant}</p>
|
||||||
|
<button onClick={store.doMagic} id={BUTTON_ID}>
|
||||||
|
do magic
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Horizon.render(<App />, container);
|
||||||
|
|
||||||
|
expect(document.getElementById(RESULT_ID).innerHTML).toBe('5');
|
||||||
|
|
||||||
|
Horizon.act(() => {
|
||||||
|
triggerClickEvent(container, BUTTON_ID);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(document.getElementById(RESULT_ID).innerHTML).toBe('9');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use parametric getters', () => {
|
||||||
|
const useArrayStore = createStore({
|
||||||
|
state: {
|
||||||
|
items: ['a', 'b', 'c'],
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
setItem: (state, index, value) => (state.items[index] = value),
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
getItem: state => index => state.items[index],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const store = useArrayStore();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p id={RESULT_ID}>{store.getItem(0) + store.getItem(1) + store.getItem(2)}</p>
|
||||||
|
<button
|
||||||
|
id={BUTTON_ID}
|
||||||
|
onClick={() => {
|
||||||
|
store.setItem(0, 'd');
|
||||||
|
store.setItem(1, 'e');
|
||||||
|
store.setItem(2, 'f');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
change
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Horizon.render(<App />, container);
|
||||||
|
expect(document.getElementById(RESULT_ID).innerHTML).toBe('abc');
|
||||||
|
|
||||||
|
Horizon.act(() => {
|
||||||
|
triggerClickEvent(container, BUTTON_ID);
|
||||||
|
});
|
||||||
|
expect(document.getElementById(RESULT_ID).innerHTML).toBe('def');
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,89 @@
|
||||||
|
import * as Horizon from '@cloudsop/horizon/index.ts';
|
||||||
|
import { createStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler';
|
||||||
|
import { triggerClickEvent } from '../../jest/commonComponents';
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
const BUTTON_ID = 'btn';
|
||||||
|
const RESET_ID = 'reset';
|
||||||
|
const RESULT_ID = 'result';
|
||||||
|
|
||||||
|
const useCounter = createStore({
|
||||||
|
state: {
|
||||||
|
counter: 0,
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
increment: function(state) {
|
||||||
|
state.counter++;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
container = document.createElement('div');
|
||||||
|
document.body.appendChild(container);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
unmountComponentAtNode(container);
|
||||||
|
container.remove();
|
||||||
|
container = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should reset to default state', async () => {
|
||||||
|
function App() {
|
||||||
|
const store = useCounter();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p id={RESULT_ID}>{store.$state.counter}</p>
|
||||||
|
<button onClick={store.increment} id={BUTTON_ID}>
|
||||||
|
add
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
store.$reset();
|
||||||
|
}}
|
||||||
|
id={RESET_ID}
|
||||||
|
>
|
||||||
|
reset
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Horizon.render(<App />, container);
|
||||||
|
|
||||||
|
Horizon.act(() => {
|
||||||
|
triggerClickEvent(container, BUTTON_ID);
|
||||||
|
});
|
||||||
|
|
||||||
|
Horizon.act(() => {
|
||||||
|
triggerClickEvent(container, BUTTON_ID);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(document.getElementById(RESULT_ID).innerHTML).toBe('2');
|
||||||
|
|
||||||
|
Horizon.act(() => {
|
||||||
|
triggerClickEvent(container, RESET_ID);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(document.getElementById(RESULT_ID).innerHTML).toBe('0');
|
||||||
|
|
||||||
|
Horizon.act(() => {
|
||||||
|
triggerClickEvent(container, BUTTON_ID);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(document.getElementById(RESULT_ID).innerHTML).toBe('1');
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { createStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler';
|
||||||
|
|
||||||
|
export const useLogStore = createStore({
|
||||||
|
id: 'logStore', // you do not need to specify ID for local store
|
||||||
|
state: {
|
||||||
|
logs: ['log'],
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
addLog: (state, data) => {
|
||||||
|
state.logs.push(data);
|
||||||
|
},
|
||||||
|
removeLog: (state, index) => {
|
||||||
|
state.logs.splice(index, 1);
|
||||||
|
},
|
||||||
|
cleanLog: state => {
|
||||||
|
state.logs.length = 0;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
length: state => {
|
||||||
|
return state.logs.length;
|
||||||
|
},
|
||||||
|
log: state => index => state.logs[index],
|
||||||
|
},
|
||||||
|
});
|
|
@ -0,0 +1,208 @@
|
||||||
|
import {
|
||||||
|
createStore,
|
||||||
|
applyMiddleware,
|
||||||
|
combineReducers,
|
||||||
|
bindActionCreators
|
||||||
|
} from '../../../../libs/horizon/src/horizonx/adapters/redux';
|
||||||
|
|
||||||
|
describe('Redux adapter', () => {
|
||||||
|
it('should use getState()', async () => {
|
||||||
|
const reduxStore = createStore((state, action) => {
|
||||||
|
return state;
|
||||||
|
}, 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}
|
||||||
|
default:
|
||||||
|
return {counter: 0};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(reduxStore.getState().counter).toBe(0);
|
||||||
|
|
||||||
|
reduxStore.dispatch({type: 'ADD'});
|
||||||
|
|
||||||
|
expect(reduxStore.getState().counter).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should attach and detach listeners', async () => {
|
||||||
|
let counter = 0;
|
||||||
|
const reduxStore = createStore((state = 0, action) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case('ADD'):
|
||||||
|
return state + 1
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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'});
|
||||||
|
expect(counter).toBe(2);
|
||||||
|
expect(reduxStore.getState()).toBe(3);
|
||||||
|
unsubscribe();
|
||||||
|
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) => {
|
||||||
|
return {
|
||||||
|
type: 'ADD_TODO',
|
||||||
|
text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const reduxStore = createStore((state = [], action) => {
|
||||||
|
if (action.type === 'ADD_TODO') {
|
||||||
|
return [...state, action.text];
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
});
|
||||||
|
|
||||||
|
const actions = bindActionCreators({addTodo}, reduxStore.dispatch);
|
||||||
|
|
||||||
|
actions.addTodo('todo');
|
||||||
|
|
||||||
|
expect(reduxStore.getState()[0]).toBe('todo');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should replace reducer', async () => {
|
||||||
|
const reduxStore = createStore((state, action) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case('ADD'):
|
||||||
|
return {counter: state.counter + 1}
|
||||||
|
default:
|
||||||
|
return {counter: 0};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
reduxStore.dispatch({type: 'ADD'});
|
||||||
|
|
||||||
|
expect(reduxStore.getState().counter).toBe(1);
|
||||||
|
|
||||||
|
reduxStore.replaceReducer((state, action) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case('SUB'):
|
||||||
|
return {counter: state.counter - 1}
|
||||||
|
default:
|
||||||
|
return {counter: 0};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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
|
||||||
|
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'});
|
||||||
|
|
||||||
|
expect(reduxStore.getState().counter).toBe(1);
|
||||||
|
expect(reduxStore.getState().check).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should apply enhancers', async () => {
|
||||||
|
let counter = 0;
|
||||||
|
let middlewareCallList = [];
|
||||||
|
|
||||||
|
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));
|
||||||
|
|
||||||
|
reduxStore.dispatch({type: 'toggle'});
|
||||||
|
reduxStore.dispatch({type: 'toggle'});
|
||||||
|
|
||||||
|
expect(counter).toBe(3); // NOTE: first action is always store initialization
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should apply multiple enhancers', async () => {
|
||||||
|
let counter = 0;
|
||||||
|
let lastAction = '';
|
||||||
|
let middlewareCallList = [];
|
||||||
|
|
||||||
|
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));
|
||||||
|
|
||||||
|
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");
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,96 @@
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,358 @@
|
||||||
|
import horizon, * as Horizon from '@cloudsop/horizon/index.ts';
|
||||||
|
import {
|
||||||
|
batch,
|
||||||
|
connect,
|
||||||
|
createStore,
|
||||||
|
Provider,
|
||||||
|
useDispatch,
|
||||||
|
useSelector,
|
||||||
|
useStore,
|
||||||
|
createSelectorHook,
|
||||||
|
createDispatchHook,
|
||||||
|
} from '../../../../libs/horizon/src/horizonx/adapters/redux';
|
||||||
|
import { triggerClickEvent } from '../../jest/commonComponents';
|
||||||
|
|
||||||
|
const BUTTON = 'button';
|
||||||
|
const BUTTON2 = 'button2';
|
||||||
|
const RESULT = 'result';
|
||||||
|
const CONTAINER = 'container';
|
||||||
|
|
||||||
|
function getE(id) {
|
||||||
|
return document.getElementById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Redux/React binding adapter', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
const container = document.createElement('div');
|
||||||
|
container.id = CONTAINER;
|
||||||
|
document.body.appendChild(container);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
document.body.removeChild(getE(CONTAINER));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should create provider context', async () => {
|
||||||
|
const reduxStore = createStore((state = 'state', action) => state);
|
||||||
|
|
||||||
|
const Child = () => {
|
||||||
|
const store = useStore();
|
||||||
|
return <div id={RESULT}>{store.getState()}</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Wrapper = () => {
|
||||||
|
return (
|
||||||
|
<Provider store={reduxStore}>
|
||||||
|
<Child />
|
||||||
|
</Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Horizon.render(<Wrapper />, getE(CONTAINER));
|
||||||
|
|
||||||
|
expect(getE(RESULT).innerHTML).toBe('state');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should use dispatch', async () => {
|
||||||
|
const reduxStore = createStore((state = 0, action) => {
|
||||||
|
if (action.type === 'ADD') return state + 1;
|
||||||
|
return state;
|
||||||
|
});
|
||||||
|
|
||||||
|
const Child = () => {
|
||||||
|
const store = useStore();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p id={RESULT}>{store.getState()}</p>
|
||||||
|
<button
|
||||||
|
id={BUTTON}
|
||||||
|
onClick={() => {
|
||||||
|
dispatch({ type: 'ADD' });
|
||||||
|
}}
|
||||||
|
></button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Wrapper = () => {
|
||||||
|
return (
|
||||||
|
<Provider store={reduxStore}>
|
||||||
|
<Child />
|
||||||
|
</Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Horizon.render(<Wrapper />, getE(CONTAINER));
|
||||||
|
|
||||||
|
expect(reduxStore.getState()).toBe(0);
|
||||||
|
|
||||||
|
Horizon.act(() => {
|
||||||
|
triggerClickEvent(getE(CONTAINER), BUTTON);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(reduxStore.getState()).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should use selector', async () => {
|
||||||
|
const reduxStore = createStore((state = 0, action) => {
|
||||||
|
if (action.type === 'ADD') return state + 1;
|
||||||
|
return state;
|
||||||
|
});
|
||||||
|
|
||||||
|
const Child = () => {
|
||||||
|
const count = useSelector(state => state);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p id={RESULT}>{count}</p>
|
||||||
|
<button
|
||||||
|
id={BUTTON}
|
||||||
|
onClick={() => {
|
||||||
|
dispatch({ type: 'ADD' });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
click
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Wrapper = () => {
|
||||||
|
return (
|
||||||
|
<Provider store={reduxStore}>
|
||||||
|
<Child />
|
||||||
|
</Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Horizon.render(<Wrapper />, getE(CONTAINER));
|
||||||
|
|
||||||
|
expect(getE(RESULT).innerHTML).toBe('0');
|
||||||
|
|
||||||
|
Horizon.act(() => {
|
||||||
|
triggerClickEvent(getE(CONTAINER), BUTTON);
|
||||||
|
triggerClickEvent(getE(CONTAINER), BUTTON);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getE(RESULT).innerHTML).toBe('2');
|
||||||
|
});
|
||||||
|
|
||||||
|
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 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 n = props.stateProps.negative;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div id={RESULT}>
|
||||||
|
{n ? '-' : '+'}
|
||||||
|
{props.stateProps.value}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
id={BUTTON}
|
||||||
|
onClick={() => {
|
||||||
|
props.dispatchProps.increment();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
add {props.ownProps.amount}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const Wrapper = () => {
|
||||||
|
const [amount, setAmount] = Horizon.useState(5);
|
||||||
|
return (
|
||||||
|
<Provider store={reduxStore}>
|
||||||
|
<Child amount={amount} />
|
||||||
|
<button
|
||||||
|
id={BUTTON2}
|
||||||
|
onClick={() => {
|
||||||
|
setAmount(3);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
change amount
|
||||||
|
</button>
|
||||||
|
</Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Horizon.render(<Wrapper />, getE(CONTAINER));
|
||||||
|
|
||||||
|
expect(getE(RESULT).innerHTML).toBe('+0');
|
||||||
|
|
||||||
|
Horizon.act(() => {
|
||||||
|
triggerClickEvent(getE(CONTAINER), BUTTON);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getE(RESULT).innerHTML).toBe('+5');
|
||||||
|
|
||||||
|
Horizon.act(() => {
|
||||||
|
triggerClickEvent(getE(CONTAINER), BUTTON2);
|
||||||
|
});
|
||||||
|
|
||||||
|
Horizon.act(() => {
|
||||||
|
triggerClickEvent(getE(CONTAINER), BUTTON);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getE(RESULT).innerHTML).toBe('+8');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should batch dispatches', async () => {
|
||||||
|
const reduxStore = createStore((state = 0, action) => {
|
||||||
|
if (action.type == 'ADD') return state + 1;
|
||||||
|
return state;
|
||||||
|
});
|
||||||
|
|
||||||
|
let renderCounter = 0;
|
||||||
|
|
||||||
|
function Counter() {
|
||||||
|
renderCounter++;
|
||||||
|
|
||||||
|
const value = useSelector(state => state);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p id={RESULT}>{value}</p>
|
||||||
|
<button
|
||||||
|
id={BUTTON}
|
||||||
|
onClick={() => {
|
||||||
|
batch(() => {
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
dispatch({ type: 'ADD' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
></button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Horizon.render(
|
||||||
|
<Provider store={reduxStore}>
|
||||||
|
<Counter />
|
||||||
|
</Provider>,
|
||||||
|
getE(CONTAINER)
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(getE(RESULT).innerHTML).toBe('0');
|
||||||
|
expect(renderCounter).toBe(1);
|
||||||
|
|
||||||
|
Horizon.act(() => {
|
||||||
|
triggerClickEvent(getE(CONTAINER), BUTTON);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getE(RESULT).innerHTML).toBe('10');
|
||||||
|
expect(renderCounter).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should use multiple contexts', async () => {
|
||||||
|
const counterStore = createStore((state = 0, action) => {
|
||||||
|
if (action.type === 'ADD') return state + 1;
|
||||||
|
return state;
|
||||||
|
});
|
||||||
|
|
||||||
|
const toggleStore = createStore((state = false, action) => {
|
||||||
|
if (action.type === 'TOGGLE') return !state;
|
||||||
|
return state;
|
||||||
|
});
|
||||||
|
|
||||||
|
const counterContext = horizon.createContext();
|
||||||
|
const toggleContext = horizon.createContext();
|
||||||
|
|
||||||
|
function Counter() {
|
||||||
|
const count = createSelectorHook(counterContext)();
|
||||||
|
const dispatch = createDispatchHook(counterContext)();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
id={BUTTON}
|
||||||
|
onClick={() => {
|
||||||
|
dispatch({ type: 'ADD' });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{count}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Toggle() {
|
||||||
|
const check = createSelectorHook(toggleContext)();
|
||||||
|
const dispatch = createDispatchHook(toggleContext)();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
id={BUTTON2}
|
||||||
|
onClick={() => {
|
||||||
|
dispatch({ type: 'TOGGLE' });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{check ? 'true' : 'false'}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Wrapper() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Provider store={counterStore} context={counterContext}>
|
||||||
|
<Counter />
|
||||||
|
</Provider>
|
||||||
|
|
||||||
|
<Provider store={toggleStore} context={toggleContext}>
|
||||||
|
<Toggle />
|
||||||
|
</Provider>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Horizon.render(<Wrapper />, getE(CONTAINER));
|
||||||
|
|
||||||
|
expect(getE(BUTTON).innerHTML).toBe('0');
|
||||||
|
expect(getE(BUTTON2).innerHTML).toBe('false');
|
||||||
|
|
||||||
|
Horizon.act(() => {
|
||||||
|
triggerClickEvent(getE(CONTAINER), BUTTON);
|
||||||
|
triggerClickEvent(getE(CONTAINER), BUTTON2);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getE(BUTTON).innerHTML).toBe('1');
|
||||||
|
expect(getE(BUTTON2).innerHTML).toBe('true');
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,69 @@
|
||||||
|
import * as Horizon from '@cloudsop/horizon/index.ts';
|
||||||
|
import { clearStore, createStore, useStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler';
|
||||||
|
import { Text } from '../../jest/commonComponents';
|
||||||
|
|
||||||
|
describe('测试 Class VNode 清除时,对引用清除', () => {
|
||||||
|
const { unmountComponentAtNode } = Horizon;
|
||||||
|
let container = null;
|
||||||
|
let globalState = {
|
||||||
|
name: 'bing dun dun',
|
||||||
|
isWin: true,
|
||||||
|
isShow: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// 创建一个 DOM 元素作为渲染目标
|
||||||
|
container = document.createElement('div');
|
||||||
|
document.body.appendChild(container);
|
||||||
|
|
||||||
|
createStore({
|
||||||
|
id: 'user',
|
||||||
|
state: globalState,
|
||||||
|
actions: {
|
||||||
|
setWin: (state, val) => {
|
||||||
|
state.isWin = val;
|
||||||
|
},
|
||||||
|
hide: state => {
|
||||||
|
state.isShow = false;
|
||||||
|
},
|
||||||
|
updateName: (state, val) => {
|
||||||
|
state.name = val;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// 退出时进行清理
|
||||||
|
unmountComponentAtNode(container);
|
||||||
|
container.remove();
|
||||||
|
container = null;
|
||||||
|
|
||||||
|
clearStore('user');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('test observer.clearByNode', () => {
|
||||||
|
class Child extends Horizon.Component {
|
||||||
|
userStore = useStore('user');
|
||||||
|
|
||||||
|
render() {
|
||||||
|
// 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 (
|
||||||
|
<div>
|
||||||
|
<Text id={'name'} text={`name: ${this.userStore.name}`} />
|
||||||
|
<Text id={'isWin'} text={`isWin: ${this.userStore.isWin}`} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
Horizon.render(<Child />, container);
|
||||||
|
}).toThrow(
|
||||||
|
'The number of updates exceeds the upper limit 50.\n' +
|
||||||
|
' A component maybe repeatedly invokes setState on componentWillUpdate or componentDidUpdate.'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,220 @@
|
||||||
|
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 (
|
||||||
|
<div>
|
||||||
|
<button id={'addBtn'} onClick={this.addOnePerson}>
|
||||||
|
add person
|
||||||
|
</button>
|
||||||
|
<button id={'delBtn'} onClick={this.delOnePerson}>
|
||||||
|
delete person
|
||||||
|
</button>
|
||||||
|
<div>{this.props.children}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
it('测试Array方法: push()、pop()', () => {
|
||||||
|
class Child extends Horizon.Component {
|
||||||
|
userStore = useStore('user');
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Text id={'hasPerson'} text={`has new person: ${this.userStore.persons.length}`} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Horizon.render(<App parent={Parent} child={Child} />, 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 (
|
||||||
|
<div>
|
||||||
|
<Text id={'nameList'} text={`name list: ${nameList.join(' ')}`} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Horizon.render(<App parent={Parent} child={Child} />, 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 (
|
||||||
|
<div>
|
||||||
|
<Text id={'nameList'} text={`name list: ${nameList.join(' ')}`} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Horizon.render(<App parent={Parent} child={Child} />, 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');
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,340 @@
|
||||||
|
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中的Map', () => {
|
||||||
|
const { unmountComponentAtNode } = Horizon;
|
||||||
|
let container = 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();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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(newPerson);
|
||||||
|
};
|
||||||
|
clearPersons = () => {
|
||||||
|
this.userStore.clearPersons();
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<button id={'addBtn'} onClick={this.addOnePerson}>
|
||||||
|
add person
|
||||||
|
</button>
|
||||||
|
<button id={'delBtn'} onClick={this.delOnePerson}>
|
||||||
|
delete person
|
||||||
|
</button>
|
||||||
|
<button id={'clearBtn'} onClick={this.clearPersons}>
|
||||||
|
clear persons
|
||||||
|
</button>
|
||||||
|
<div>{this.props.children}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
it('测试Map方法: set()、delete()、clear()', () => {
|
||||||
|
class Child extends Horizon.Component {
|
||||||
|
userStore = useStore('user');
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Text id={'size'} text={`persons number: ${this.userStore.$state.persons.size}`} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Horizon.render(<App parent={Parent} child={Child} />, container);
|
||||||
|
|
||||||
|
expect(container.querySelector('#size').innerHTML).toBe('persons number: 2');
|
||||||
|
// 在Map中增加一个对象
|
||||||
|
Horizon.act(() => {
|
||||||
|
triggerClickEvent(container, 'addBtn');
|
||||||
|
});
|
||||||
|
expect(container.querySelector('#size').innerHTML).toBe('persons number: 3');
|
||||||
|
|
||||||
|
// 在Map中删除一个对象
|
||||||
|
Horizon.act(() => {
|
||||||
|
triggerClickEvent(container, 'delBtn');
|
||||||
|
});
|
||||||
|
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');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('测试Map方法: keys()', () => {
|
||||||
|
class Child extends Horizon.Component {
|
||||||
|
userStore = useStore('user');
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const nameList = [];
|
||||||
|
const keys = this.userStore.$state.persons.keys();
|
||||||
|
for (const key of keys) {
|
||||||
|
nameList.push(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Text id={'nameList'} text={`name list: ${nameList.join(' ')}`} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Horizon.render(<App parent={Parent} child={Child} />, container);
|
||||||
|
|
||||||
|
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');
|
||||||
|
|
||||||
|
// 在Map中删除一个对象
|
||||||
|
Horizon.act(() => {
|
||||||
|
triggerClickEvent(container, 'delBtn');
|
||||||
|
});
|
||||||
|
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: ');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('测试Map方法: values()', () => {
|
||||||
|
class Child extends Horizon.Component {
|
||||||
|
userStore = useStore('user');
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const ageList = [];
|
||||||
|
const values = this.userStore.$state.persons.values();
|
||||||
|
for (const val of values) {
|
||||||
|
ageList.push(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Text id={'ageList'} text={`age list: ${ageList.join(' ')}`} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Horizon.render(<App parent={Parent} child={Child} />, container);
|
||||||
|
|
||||||
|
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');
|
||||||
|
|
||||||
|
// 在Map中删除一个对象
|
||||||
|
Horizon.act(() => {
|
||||||
|
triggerClickEvent(container, 'delBtn');
|
||||||
|
});
|
||||||
|
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: ');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('测试Map方法: entries()', () => {
|
||||||
|
class Child extends Horizon.Component {
|
||||||
|
userStore = useStore('user');
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const nameList = [];
|
||||||
|
const entries = this.userStore.$state.persons.entries();
|
||||||
|
for (const entry of entries) {
|
||||||
|
nameList.push(entry[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Text id={'nameList'} text={`name list: ${nameList.join(' ')}`} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Horizon.render(<App parent={Parent} child={Child} />, container);
|
||||||
|
|
||||||
|
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');
|
||||||
|
|
||||||
|
// 在Map中删除一个对象
|
||||||
|
Horizon.act(() => {
|
||||||
|
triggerClickEvent(container, 'delBtn');
|
||||||
|
});
|
||||||
|
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: ');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('测试Map方法: forEach()', () => {
|
||||||
|
class Child extends Horizon.Component {
|
||||||
|
userStore = useStore('user');
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const nameList = [];
|
||||||
|
this.userStore.$state.persons.forEach((val, key) => {
|
||||||
|
nameList.push(key);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Text id={'nameList'} text={`name list: ${nameList.join(' ')}`} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Horizon.render(<App parent={Parent} child={Child} />, container);
|
||||||
|
|
||||||
|
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');
|
||||||
|
|
||||||
|
// 在Map中删除一个对象
|
||||||
|
Horizon.act(() => {
|
||||||
|
triggerClickEvent(container, 'delBtn');
|
||||||
|
});
|
||||||
|
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: ');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('测试Map方法: has()', () => {
|
||||||
|
class Child extends Horizon.Component {
|
||||||
|
userStore = useStore('user');
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Text id={'hasPerson'} text={`has new person: ${this.userStore.$state.persons.has(newPerson.name)}`} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Horizon.render(<App parent={Parent} child={Child} />, container);
|
||||||
|
|
||||||
|
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');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('测试Map方法: for of()', () => {
|
||||||
|
class Child extends Horizon.Component {
|
||||||
|
userStore = useStore('user');
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const nameList = [];
|
||||||
|
for (const per of this.userStore.$state.persons) {
|
||||||
|
nameList.push(per[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Text id={'nameList'} text={`name list: ${nameList.join(' ')}`} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Horizon.render(<App parent={Parent} child={Child} />, container);
|
||||||
|
|
||||||
|
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');
|
||||||
|
|
||||||
|
// 在Map中删除一个对象
|
||||||
|
Horizon.act(() => {
|
||||||
|
triggerClickEvent(container, 'delBtn');
|
||||||
|
});
|
||||||
|
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: ');
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,119 @@
|
||||||
|
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';
|
||||||
|
|
||||||
|
describe('测试 Class VNode 清除时,对引用清除', () => {
|
||||||
|
const { unmountComponentAtNode } = Horizon;
|
||||||
|
let container = null;
|
||||||
|
let globalState = {
|
||||||
|
name: 'bing dun dun',
|
||||||
|
isWin: true,
|
||||||
|
isShow: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// 创建一个 DOM 元素作为渲染目标
|
||||||
|
container = document.createElement('div');
|
||||||
|
document.body.appendChild(container);
|
||||||
|
|
||||||
|
createStore({
|
||||||
|
id: 'user',
|
||||||
|
state: globalState,
|
||||||
|
actions: {
|
||||||
|
setWin: (state, val) => {
|
||||||
|
state.isWin = val;
|
||||||
|
},
|
||||||
|
hide: state => {
|
||||||
|
state.isShow = false;
|
||||||
|
},
|
||||||
|
updateName: (state, val) => {
|
||||||
|
state.name = val;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// 退出时进行清理
|
||||||
|
unmountComponentAtNode(container);
|
||||||
|
container.remove();
|
||||||
|
container = null;
|
||||||
|
|
||||||
|
clearStore('user');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('test observer.clearByNode', () => {
|
||||||
|
class App extends Horizon.Component {
|
||||||
|
userStore = useStore('user');
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<button id={'hideBtn'} onClick={this.userStore.hide}>
|
||||||
|
toggle
|
||||||
|
</button>
|
||||||
|
{this.userStore.isShow && <Parent />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Parent extends Horizon.Component {
|
||||||
|
userStore = useStore('user');
|
||||||
|
|
||||||
|
setWin = () => {
|
||||||
|
this.userStore.setWin(!this.userStore.isWin);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<button id={'toggleBtn'} onClick={this.setWin}>
|
||||||
|
toggle
|
||||||
|
</button>
|
||||||
|
{this.userStore.isWin && <Child />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Child extends Horizon.Component {
|
||||||
|
userStore = useStore('user');
|
||||||
|
|
||||||
|
render() {
|
||||||
|
// this.userStore.updateName(this.userStore.name === 'bing dun dun' ? 'huo dun dun' : 'bing dun dun');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Text id={'name'} text={`name: ${this.userStore.name}`} />
|
||||||
|
<Text id={'isWin'} text={`isWin: ${this.userStore.isWin}`} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Horizon.render(<App />, container);
|
||||||
|
|
||||||
|
// Parent and Child hold the isWin key
|
||||||
|
expect(getObserver(globalState).keyVNodes.get('isWin').size).toBe(2);
|
||||||
|
|
||||||
|
Horizon.act(() => {
|
||||||
|
triggerClickEvent(container, 'toggleBtn');
|
||||||
|
});
|
||||||
|
// Parent hold the isWin key
|
||||||
|
expect(getObserver(globalState).keyVNodes.get('isWin').size).toBe(1);
|
||||||
|
|
||||||
|
Horizon.act(() => {
|
||||||
|
triggerClickEvent(container, 'toggleBtn');
|
||||||
|
});
|
||||||
|
// Parent and Child hold the isWin key
|
||||||
|
expect(getObserver(globalState).keyVNodes.get('isWin').size).toBe(2);
|
||||||
|
|
||||||
|
Horizon.act(() => {
|
||||||
|
triggerClickEvent(container, 'hideBtn');
|
||||||
|
});
|
||||||
|
// no component hold the isWin key
|
||||||
|
expect(getObserver(globalState).keyVNodes.get('isWin')).toBe(undefined);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,114 @@
|
||||||
|
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';
|
||||||
|
|
||||||
|
describe('测试VNode清除时,对引用清除', () => {
|
||||||
|
const { unmountComponentAtNode } = Horizon;
|
||||||
|
let container = null;
|
||||||
|
let globalState = {
|
||||||
|
name: 'bing dun dun',
|
||||||
|
isWin: true,
|
||||||
|
isShow: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// 创建一个 DOM 元素作为渲染目标
|
||||||
|
container = document.createElement('div');
|
||||||
|
document.body.appendChild(container);
|
||||||
|
|
||||||
|
createStore({
|
||||||
|
id: 'user',
|
||||||
|
state: globalState,
|
||||||
|
actions: {
|
||||||
|
setWin: (state, val) => {
|
||||||
|
state.isWin = val;
|
||||||
|
},
|
||||||
|
hide: state => {
|
||||||
|
state.isShow = false;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// 退出时进行清理
|
||||||
|
unmountComponentAtNode(container);
|
||||||
|
container.remove();
|
||||||
|
container = null;
|
||||||
|
|
||||||
|
clearStore('user');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('test observer.clearByNode', () => {
|
||||||
|
class App extends Horizon.Component {
|
||||||
|
userStore = useStore('user');
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<button id={'hideBtn'} onClick={this.userStore.hide}>
|
||||||
|
toggle
|
||||||
|
</button>
|
||||||
|
{this.userStore.isShow && <Parent />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Parent extends Horizon.Component {
|
||||||
|
userStore = useStore('user');
|
||||||
|
|
||||||
|
setWin = () => {
|
||||||
|
this.userStore.setWin(!this.userStore.isWin);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<button id={'toggleBtn'} onClick={this.setWin}>
|
||||||
|
toggle
|
||||||
|
</button>
|
||||||
|
{this.userStore.isWin && <Child />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Child extends Horizon.Component {
|
||||||
|
userStore = useStore('user');
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Text id={'name'} text={`name: ${this.userStore.name}`} />
|
||||||
|
<Text id={'isWin'} text={`isWin: ${this.userStore.isWin}`} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Horizon.render(<App />, container);
|
||||||
|
|
||||||
|
// Parent and Child hold the isWin key
|
||||||
|
expect(getObserver(globalState).keyVNodes.get('isWin').size).toBe(2);
|
||||||
|
|
||||||
|
Horizon.act(() => {
|
||||||
|
triggerClickEvent(container, 'toggleBtn');
|
||||||
|
});
|
||||||
|
// Parent hold the isWin key
|
||||||
|
expect(getObserver(globalState).keyVNodes.get('isWin').size).toBe(1);
|
||||||
|
|
||||||
|
Horizon.act(() => {
|
||||||
|
triggerClickEvent(container, 'toggleBtn');
|
||||||
|
});
|
||||||
|
// Parent and Child hold the isWin key
|
||||||
|
expect(getObserver(globalState).keyVNodes.get('isWin').size).toBe(2);
|
||||||
|
|
||||||
|
Horizon.act(() => {
|
||||||
|
triggerClickEvent(container, 'hideBtn');
|
||||||
|
});
|
||||||
|
// no component hold the isWin key
|
||||||
|
expect(getObserver(globalState).keyVNodes.get('isWin')).toBe(undefined);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,21 @@
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,9 +1,8 @@
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
import * as Horizon from '@cloudsop/horizon/index.ts';
|
import * as Horizon from '@cloudsop/horizon/index.ts';
|
||||||
import { getLogUtils } from './testUtils';
|
import { getLogUtils } from './testUtils';
|
||||||
|
|
||||||
export const App = (props) => {
|
export const App = props => {
|
||||||
const Parent = props.parent;
|
const Parent = props.parent;
|
||||||
const Child = props.child;
|
const Child = props.child;
|
||||||
|
|
||||||
|
@ -16,8 +15,15 @@ export const App = (props) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Text = (props) => {
|
export const Text = props => {
|
||||||
const LogUtils =getLogUtils();
|
const LogUtils = getLogUtils();
|
||||||
LogUtils.log(props.text);
|
LogUtils.log(props.text);
|
||||||
return <p id={props.id}>{props.text}</p>;
|
return <p id={props.id}>{props.text}</p>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function triggerClickEvent(container, id) {
|
||||||
|
const event = new MouseEvent('click', {
|
||||||
|
bubbles: true,
|
||||||
|
});
|
||||||
|
container.querySelector(`#${id}`).dispatchEvent(event);
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import nodeResolve from '@rollup/plugin-node-resolve';
|
import nodeResolve from '@rollup/plugin-node-resolve';
|
||||||
import babel from '@rollup/plugin-babel';
|
import babel from '@rollup/plugin-babel';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
import fs from 'fs';
|
||||||
import replace from '@rollup/plugin-replace';
|
import replace from '@rollup/plugin-replace';
|
||||||
import copy from './copy-plugin';
|
import copy from './copy-plugin';
|
||||||
import { terser } from 'rollup-plugin-terser';
|
import { terser } from 'rollup-plugin-terser';
|
||||||
|
@ -11,6 +12,14 @@ const extensions = ['.js', '.ts'];
|
||||||
const libDir = path.join(__dirname, '../../libs/horizon');
|
const libDir = path.join(__dirname, '../../libs/horizon');
|
||||||
const rootDir = path.join(__dirname, '../..');
|
const rootDir = path.join(__dirname, '../..');
|
||||||
const outDir = path.join(rootDir, 'build', 'horizon');
|
const outDir = path.join(rootDir, 'build', 'horizon');
|
||||||
|
|
||||||
|
if (!fs.existsSync(path.join(rootDir, 'build'))) {
|
||||||
|
fs.mkdirSync(path.join(rootDir, 'build'));
|
||||||
|
}
|
||||||
|
if (!fs.existsSync(outDir)) {
|
||||||
|
fs.mkdirSync(outDir);
|
||||||
|
}
|
||||||
|
|
||||||
const outputResolve = (...p) => path.resolve(outDir, ...p);
|
const outputResolve = (...p) => path.resolve(outDir, ...p);
|
||||||
|
|
||||||
function genConfig(mode) {
|
function genConfig(mode) {
|
||||||
|
|
Loading…
Reference in New Issue