Match-id-7ba267bb05c479c55ce061cc65e484acd18b2fcd

This commit is contained in:
* 2022-06-23 10:50:39 +08:00 committed by *
commit e8c1773c59
40 changed files with 4541 additions and 11 deletions

View File

@ -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;

View File

@ -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);
}
}

View File

@ -0,0 +1,3 @@
// The two constants must be the same as those in horizon.
export const FunctionComponent = 'FunctionComponent';
export const ClassComponent = 'ClassComponent';

View File

@ -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();
}

View File

@ -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;
};
}

View File

@ -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;

View File

@ -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 {
}
}

View File

@ -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);
}
}

View File

@ -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];
}

View File

@ -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;
}

View File

@ -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;
},
};
}

View File

@ -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;
}

View File

@ -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;

View File

@ -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);
}

75
libs/horizon/src/horizonx/types.d.ts vendored Normal file
View File

@ -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;

View File

@ -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');
});
});

View File

@ -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: ');
});
});

View File

@ -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');
});
});

View File

@ -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: ');
});
});

View File

@ -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');
});
});

View File

@ -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');
});
});

View File

@ -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);
});
});

View File

@ -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');
});
});

View File

@ -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');
});
});

View File

@ -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');
});
});

View File

@ -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');
});
});

View File

@ -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],
},
});

View File

@ -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");
});
});

View File

@ -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;
};
};
}

View File

@ -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);
});
});

View File

@ -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');
});
});

View File

@ -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.'
);
});
});

View File

@ -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');
});
});

View File

@ -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: ');
});
});

View File

@ -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);
});
});

View File

@ -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);
});
});

View File

@ -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);
});
});

View File

@ -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);
}

View File

@ -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) {