From d14122a5d3ef55ba66dfbabb9d196eace17e58c2 Mon Sep 17 00:00:00 2001 From: * <8> Date: Wed, 20 Apr 2022 14:47:42 +0800 Subject: [PATCH 01/13] Match-id-c512ddd2eafdc01bfd6b07d73a3aead7828578e8 --- libs/horizon/src/dom/DOMInternalKeys.ts | 24 +++++-------- .../dom/DOMPropertiesHandler/StyleHandler.ts | 22 ++++++------ .../src/dom/validators/ValidateProps.ts | 2 +- libs/horizon/src/dom/valueHandler/index.ts | 2 +- libs/horizon/src/event/HorizonEventMain.ts | 17 +++++---- libs/horizon/src/renderer/vnode/VNodeUtils.ts | 35 ++++++------------- scripts/__tests__/DomTest/Attribute.test.js | 8 ++++- scripts/__tests__/DomTest/DomInput.test.js | 7 +++- 8 files changed, 54 insertions(+), 63 deletions(-) diff --git a/libs/horizon/src/dom/DOMInternalKeys.ts b/libs/horizon/src/dom/DOMInternalKeys.ts index 2f4ccb23..f389a0cf 100644 --- a/libs/horizon/src/dom/DOMInternalKeys.ts +++ b/libs/horizon/src/dom/DOMInternalKeys.ts @@ -49,23 +49,17 @@ export function getVNode(dom: Node|Container): VNode | null { // 用 DOM 对象,来寻找其对应或者说是最近父级的 vNode export function getNearestVNode(dom: Node): null | VNode { - let vNode = dom[INTERNAL_VNODE]; - if (vNode) { // 如果是已经被框架标记过的 DOM 节点,那么直接返回其 VNode 实例 - return vNode; + let domNode: Node | null = dom; + // 寻找当前节点及其所有祖先节点是否有标记VNODE + while (domNode) { + const vNode = domNode[INTERNAL_VNODE]; + if (vNode) { + return vNode; + } + domNode = domNode.parentNode; } - // 下面处理的是为被框架标记过的 DOM 节点,向上找其父节点是否被框架标记过 - let parentDom = dom.parentNode; - let nearVNode = null; - while (parentDom) { - vNode = parentDom[INTERNAL_VNODE]; - if (vNode) { - nearVNode = vNode; - break; - } - parentDom = parentDom.parentNode; - } - return nearVNode; + return null; } // 获取 vNode 上的属性相关信息 diff --git a/libs/horizon/src/dom/DOMPropertiesHandler/StyleHandler.ts b/libs/horizon/src/dom/DOMPropertiesHandler/StyleHandler.ts index 49429fbe..b93e1581 100644 --- a/libs/horizon/src/dom/DOMPropertiesHandler/StyleHandler.ts +++ b/libs/horizon/src/dom/DOMPropertiesHandler/StyleHandler.ts @@ -1,12 +1,12 @@ -function isNeedUnitCSS(propName: string) { - return !(noUnitCSS.includes(propName) - || propName.startsWith('borderImage') - || propName.startsWith('flex') - || propName.startsWith('gridRow') - || propName.startsWith('gridColumn') - || propName.startsWith('stroke') - || propName.startsWith('box') - || propName.endsWith('Opacity')); +function isNeedUnitCSS(styleName: string) { + return !(noUnitCSS.includes(styleName) + || styleName.startsWith('borderImage') + || styleName.startsWith('flex') + || styleName.startsWith('gridRow') + || styleName.startsWith('gridColumn') + || styleName.startsWith('stroke') + || styleName.startsWith('box') + || styleName.endsWith('Opacity')); } /** @@ -38,9 +38,7 @@ export function setStyles(dom, styles) { Object.keys(styles).forEach((name) => { const styleVal = styles[name]; - const validStyleValue = adjustStyleValue(name, styleVal); - - style[name] = validStyleValue; + style[name] = adjustStyleValue(name, styleVal); }); } diff --git a/libs/horizon/src/dom/validators/ValidateProps.ts b/libs/horizon/src/dom/validators/ValidateProps.ts index 5616f342..9dcf4a23 100644 --- a/libs/horizon/src/dom/validators/ValidateProps.ts +++ b/libs/horizon/src/dom/validators/ValidateProps.ts @@ -6,7 +6,7 @@ const INVALID_EVENT_NAME_REGEX = /^on[^A-Z]/; // 是内置元素 -export function isNativeElement(tagName: string, props: Object) { +export function isNativeElement(tagName: string, props: Record) { return !tagName.includes('-') && props.is === undefined; } diff --git a/libs/horizon/src/dom/valueHandler/index.ts b/libs/horizon/src/dom/valueHandler/index.ts index 88a35e5c..2e5b6da7 100644 --- a/libs/horizon/src/dom/valueHandler/index.ts +++ b/libs/horizon/src/dom/valueHandler/index.ts @@ -21,7 +21,7 @@ import { getTextareaPropsWithoutValue, updateTextareaValue, } from './TextareaValueHandler'; -import {getDomTag} from "../utils/Common"; +import {getDomTag} from '../utils/Common'; // 获取元素除了被代理的值以外的属性 function getPropsWithoutValue(type: string, dom: HorizonDom, properties: IProperty) { diff --git a/libs/horizon/src/event/HorizonEventMain.ts b/libs/horizon/src/event/HorizonEventMain.ts index 08e9f5d0..f0480724 100644 --- a/libs/horizon/src/event/HorizonEventMain.ts +++ b/libs/horizon/src/event/HorizonEventMain.ts @@ -16,8 +16,11 @@ import { decorateNativeEvent } from './customEvents/EventFactory'; import { getListenersFromTree } from './ListenerGetter'; import { shouldUpdateValue, updateControlledValue } from './ControlledValueUpdater'; import { asyncUpdates, runDiscreteUpdates } from '../renderer/Renderer'; -import { getExactNode } from '../renderer/vnode/VNodeUtils'; -import {ListenerUnitList} from './Types'; +import { findRoot } from '../renderer/vnode/VNodeUtils'; +import { ListenerUnitList } from './Types'; + +// web规范,鼠标右键key值 +const RIGHT_MOUSE_BUTTON = 2; // 获取事件触发的普通事件监听方法队列 function getCommonListeners( @@ -29,13 +32,13 @@ function getCommonListeners( ): ListenerUnitList { const name = CommonEventToHorizonMap[nativeEvtName]; const horizonEvtName = !name ? '' : `on${name[0].toUpperCase()}${name.slice(1)}`; // 例:dragEnd -> onDragEnd - + if (!horizonEvtName) { return []; } // 鼠标点击右键 - if (nativeEvent instanceof MouseEvent && nativeEvtName === 'click' && nativeEvent.button === 2) { + if (nativeEvent instanceof MouseEvent && nativeEvtName === 'click' && nativeEvent.button === RIGHT_MOUSE_BUTTON) { return []; } @@ -76,7 +79,7 @@ function getProcessListeners( vNode: VNode | null, nativeEvent: AnyNativeEvent, target, - isCapture: boolean + isCapture: boolean, ): ListenerUnitList { // 触发普通委托事件 let listenerList: ListenerUnitList = getCommonListeners( @@ -136,11 +139,11 @@ export function handleEventMain( isCapture: boolean, nativeEvent: AnyNativeEvent, vNode: null | VNode, - targetContainer: EventTarget, + targetDom: EventTarget, ): void { let startVNode = vNode; if (startVNode !== null) { - startVNode = getExactNode(startVNode, targetContainer); + startVNode = findRoot(startVNode, targetDom); if (!startVNode) { return; } diff --git a/libs/horizon/src/renderer/vnode/VNodeUtils.ts b/libs/horizon/src/renderer/vnode/VNodeUtils.ts index ef60b566..a586b922 100644 --- a/libs/horizon/src/renderer/vnode/VNodeUtils.ts +++ b/libs/horizon/src/renderer/vnode/VNodeUtils.ts @@ -31,7 +31,6 @@ export function travelVNodeTree( finishVNode: VNode, // 结束遍历节点,有时候和beginVNode不相同 handleWhenToParent: Function | null ): VNode | null { - const filter = childFilter === null; let node = beginVNode; while (true) { @@ -43,7 +42,7 @@ export function travelVNodeTree( // 找子节点 const childVNode = node.child; - if (childVNode !== null && (filter || !childFilter(node))) { + if (childVNode !== null && (childFilter === null || !childFilter(node))) { childVNode.parent = node; node = childVNode; continue; @@ -194,20 +193,6 @@ export function getSiblingDom(vNode: VNode): Element | null { } } -function isSameContainer( - container: Element, - targetContainer: EventTarget, -): boolean { - if (container === targetContainer) { - return true; - } - // 注释类型的节点 - if (isComment(container) && container.parentNode === targetContainer) { - return true; - } - return false; -} - function isPortalRoot(vNode, targetContainer) { if (vNode.tag === DomPortal) { let topVNode = vNode.parent; @@ -216,7 +201,7 @@ function isPortalRoot(vNode, targetContainer) { if (grandTag === TreeRoot || grandTag === DomPortal) { const topContainer = topVNode.realNode; // 如果topContainer是targetContainer,不需要在这里处理 - if (isSameContainer(topContainer, targetContainer)) { + if (topContainer === targetContainer) { return true; } } @@ -228,28 +213,28 @@ function isPortalRoot(vNode, targetContainer) { } // 获取根vNode节点 -export function getExactNode(targetVNode, targetContainer) { +export function findRoot(targetVNode, targetDom) { // 确认vNode节点是否准确,portal场景下可能祖先节点不准确 let vNode = targetVNode; while (vNode !== null) { if (vNode.tag === TreeRoot || vNode.tag === DomPortal) { - let container = vNode.realNode; - if (isSameContainer(container, targetContainer)) { + let dom = vNode.realNode; + if (dom === targetDom) { break; } - if (isPortalRoot(vNode, targetContainer)) { + if (isPortalRoot(vNode, targetDom)) { return null; } - while (container !== null) { - const parentNode = getNearestVNode(container); + while (dom !== null) { + const parentNode = getNearestVNode(dom); if (parentNode === null) { return null; } if (parentNode.tag === DomComponent || parentNode.tag === DomText) { - return getExactNode(parentNode, targetContainer); + return findRoot(parentNode, targetDom); } - container = container.parentNode; + dom = dom.parentNode; } } vNode = vNode.parent; diff --git a/scripts/__tests__/DomTest/Attribute.test.js b/scripts/__tests__/DomTest/Attribute.test.js index 49030e2f..32fe44a5 100755 --- a/scripts/__tests__/DomTest/Attribute.test.js +++ b/scripts/__tests__/DomTest/Attribute.test.js @@ -61,4 +61,10 @@ describe('Dom Attribute', () => { container.querySelector('div').setAttribute('data-first-name', 'Tom'); expect(container.querySelector('div').dataset.firstName).toBe('Tom'); }); -}); \ No newline at end of file + + it('style 自动加px', () => { + const div = Horizon.render(
, container); + expect(window.getComputedStyle(div).getPropertyValue('width')).toBe('10px'); + expect(window.getComputedStyle(div).getPropertyValue('height')).toBe('20px'); + }); +}); diff --git a/scripts/__tests__/DomTest/DomInput.test.js b/scripts/__tests__/DomTest/DomInput.test.js index d5649ff3..7d43b251 100755 --- a/scripts/__tests__/DomTest/DomInput.test.js +++ b/scripts/__tests__/DomTest/DomInput.test.js @@ -172,6 +172,11 @@ describe('Dom Input', () => { expect(realNode.getAttribute('value')).toBe('default'); }); + it('value为0、defaultValue为1,input 的value应该为0', () => { + const input = Horizon.render(, container); + expect(input.getAttribute('value')).toBe('0'); + }); + it('name属性', () => { let realNode = Horizon.render(, container); expect(realNode.name).toBe('name'); @@ -426,4 +431,4 @@ describe('Dom Input', () => { expect(container.querySelector('input').hasAttribute('value')).toBe(false); }); }); -}); \ No newline at end of file +}); From c34df0d5f47a45f1e55e21e31974b6fb183d08f7 Mon Sep 17 00:00:00 2001 From: * <8> Date: Fri, 13 May 2022 11:00:00 +0800 Subject: [PATCH 02/13] Match-id-4c5a3a22ce4ffd2686921714db97c18b77f16288 --- libs/horizon/src/dom/DOMExternal.ts | 2 +- .../DOMPropertiesHandler.ts | 9 ++++- .../dom/valueHandler/SelectValueHandler.ts | 2 +- libs/horizon/src/event/EventBinding.ts | 37 +++++++++++++------ libs/horizon/src/event/HorizonEventMain.ts | 11 +----- libs/horizon/src/event/const.ts | 3 +- .../simulatedEvtHandler/ChangeEventHandler.ts | 5 +++ libs/horizon/src/event/utils.ts | 6 +-- libs/horizon/src/renderer/TreeBuilder.ts | 8 +++- libs/horizon/src/renderer/submit/Submit.ts | 8 ++-- libs/horizon/src/renderer/vnode/VNode.ts | 4 ++ 11 files changed, 59 insertions(+), 36 deletions(-) diff --git a/libs/horizon/src/dom/DOMExternal.ts b/libs/horizon/src/dom/DOMExternal.ts index 077d0801..02f11e53 100644 --- a/libs/horizon/src/dom/DOMExternal.ts +++ b/libs/horizon/src/dom/DOMExternal.ts @@ -23,7 +23,7 @@ function createRoot(children: any, container: Container, callback?: Callback) { container._treeRoot = treeRoot; // 根节点挂接全量事件 - listenDelegatedEvents(container as Element); + // listenDelegatedEvents(container as Element); // 执行回调 if (typeof callback === 'function') { diff --git a/libs/horizon/src/dom/DOMPropertiesHandler/DOMPropertiesHandler.ts b/libs/horizon/src/dom/DOMPropertiesHandler/DOMPropertiesHandler.ts index c7f76f2f..1ac626a0 100644 --- a/libs/horizon/src/dom/DOMPropertiesHandler/DOMPropertiesHandler.ts +++ b/libs/horizon/src/dom/DOMPropertiesHandler/DOMPropertiesHandler.ts @@ -4,9 +4,11 @@ import { import { updateCommonProp } from './UpdateCommonProp'; import { setStyles } from './StyleHandler'; import { - listenNonDelegatedEvent + lazyDelegateOnRoot, + listenNonDelegatedEvent, } from '../../event/EventBinding'; import { isEventProp } from '../validators/ValidateProps'; +import { getCurrentRoot } from '../../renderer/TreeBuilder'; // 初始化DOM属性和更新 DOM 属性 export function setDomProps( @@ -27,8 +29,12 @@ export function setDomProps( setStyles(dom, propVal); } else if (isEventProp(propName)) { // 事件监听属性处理 + // TODO + const currentRoot = getCurrentRoot(); if (!allDelegatedHorizonEvents.has(propName)) { listenNonDelegatedEvent(propName, dom, propVal); + } else if (currentRoot && !currentRoot.delegatedEvents.has(propName)) { + lazyDelegateOnRoot(currentRoot, propName); } } else if (propName === 'children') { // 只处理纯文本子节点,其他children在VNode树中处理 const type = typeof propVal; @@ -147,6 +153,7 @@ export function compareProps( if (!allDelegatedHorizonEvents.has(propName)) { toUpdateProps[propName] = newPropValue; } + // TODO } else { toUpdateProps[propName] = newPropValue; } diff --git a/libs/horizon/src/dom/valueHandler/SelectValueHandler.ts b/libs/horizon/src/dom/valueHandler/SelectValueHandler.ts index 75812620..d7c5852f 100644 --- a/libs/horizon/src/dom/valueHandler/SelectValueHandler.ts +++ b/libs/horizon/src/dom/valueHandler/SelectValueHandler.ts @@ -43,7 +43,7 @@ export function getSelectPropsWithoutValue(dom: HorizonSelect, properties: Objec return { ...properties, value: undefined, - } + }; } export function updateSelectValue(dom: HorizonSelect, properties: IProperty, isInit: boolean = false) { diff --git a/libs/horizon/src/event/EventBinding.ts b/libs/horizon/src/event/EventBinding.ts index 54d3fdb4..66bf94a7 100644 --- a/libs/horizon/src/event/EventBinding.ts +++ b/libs/horizon/src/event/EventBinding.ts @@ -1,7 +1,7 @@ /** * 事件绑定实现,分为绑定委托事件和非委托事件 */ -import {allDelegatedNativeEvents} from './EventCollection'; +import { allDelegatedHorizonEvents, allDelegatedNativeEvents } from './EventCollection'; import {isDocument} from '../dom/utils/Common'; import { getNearestVNode, @@ -12,6 +12,7 @@ import {isMounted} from '../renderer/vnode/VNodeUtils'; import {SuspenseComponent} from '../renderer/vnode/VNodeTags'; import {handleEventMain} from './HorizonEventMain'; import {decorateNativeEvent} from './customEvents/EventFactory'; +import { VNode } from '../renderer/vnode/VNode'; const listeningMarker = '_horizonListening' + Math.random().toString(36).slice(4); @@ -26,18 +27,20 @@ function triggerDelegatedEvent( runDiscreteUpdates(); const nativeEventTarget = nativeEvent.target || nativeEvent.srcElement; - let targetVNode = getNearestVNode(nativeEventTarget); + const targetVNode = getNearestVNode(nativeEventTarget); - if (targetVNode !== null) { - if (isMounted(targetVNode)) { - if (targetVNode.tag === SuspenseComponent) { - targetVNode = null; - } - } else { - // vNode已销毁 - targetVNode = null; - } - } + // if (targetVNode !== null) { + // if (isMounted(targetVNode)) { + // if (targetVNode.tag === SuspenseComponent) { + // debugger + // targetVNode = null; + // } + // } else { + // debugger + // // vNode已销毁 + // targetVNode = null; + // } + // } handleEventMain(nativeEvtName, isCapture, nativeEvent, targetVNode, targetDom); } @@ -73,6 +76,16 @@ export function listenDelegatedEvents(dom: Element) { }); } +// 事件懒委托,当用户定义事件后,再进行委托到根节点 +export function lazyDelegateOnRoot(currentRoot: VNode, eventName: string) { + currentRoot.delegatedEvents.add(eventName); + + const isCapture = isCaptureEvent(eventName); + const nativeEvents = allDelegatedHorizonEvents.get(eventName); + nativeEvents.forEach(nativeEvents => { + listenToNativeEvent(nativeEvents, currentRoot.realNode, isCapture); + }); +} // 通过horizon事件名获取到native事件名 function getNativeEvtName(horizonEventName, capture) { let nativeName; diff --git a/libs/horizon/src/event/HorizonEventMain.ts b/libs/horizon/src/event/HorizonEventMain.ts index f0480724..35db487b 100644 --- a/libs/horizon/src/event/HorizonEventMain.ts +++ b/libs/horizon/src/event/HorizonEventMain.ts @@ -8,7 +8,6 @@ import { EVENT_TYPE_CAPTURE, } from './const'; import { getListeners as getChangeListeners } from './simulatedEvtHandler/ChangeEventHandler'; -import { getListeners as getSelectionListeners } from './simulatedEvtHandler/SelectionEventHandler'; import { setPropertyWritable, } from './utils'; @@ -81,6 +80,7 @@ function getProcessListeners( target, isCapture: boolean, ): ListenerUnitList { + // TODO 重复从树中获取监听器 // 触发普通委托事件 let listenerList: ListenerUnitList = getCommonListeners( nativeEvtName, @@ -100,15 +100,6 @@ function getProcessListeners( target, )); } - - if (horizonEventToNativeMap.get('onSelect').includes(nativeEvtName)) { - listenerList = listenerList.concat(getSelectionListeners( - nativeEvtName, - nativeEvent, - vNode, - target, - )); - } } return listenerList; } diff --git a/libs/horizon/src/event/const.ts b/libs/horizon/src/event/const.ts index 449663e9..c0e185a4 100644 --- a/libs/horizon/src/event/const.ts +++ b/libs/horizon/src/event/const.ts @@ -28,8 +28,7 @@ export const horizonEventToNativeMap = new Map([ ['onCompositionStart', ['compositionstart']], ['onCompositionUpdate', ['compositionupdate']], ['onChange', ['change', 'click', 'focusout', 'input']], - ['onSelect', ['focusout', 'contextmenu', 'dragend', 'focusin', - 'keydown', 'keyup', 'mousedown', 'mouseup', 'selectionchange']], + ['onSelect', ['select']], ['onAnimationEnd', ['animationend']], ['onAnimationIteration', ['animationiteration']], diff --git a/libs/horizon/src/event/simulatedEvtHandler/ChangeEventHandler.ts b/libs/horizon/src/event/simulatedEvtHandler/ChangeEventHandler.ts index c0ad947d..f1b781af 100644 --- a/libs/horizon/src/event/simulatedEvtHandler/ChangeEventHandler.ts +++ b/libs/horizon/src/event/simulatedEvtHandler/ChangeEventHandler.ts @@ -12,6 +12,11 @@ import {VNode} from '../../renderer/Types'; import {getDomTag} from '../../dom/utils/Common'; // 返回是否需要触发change事件标记 +// | 元素 | 事件 | 需要值变更 | +// | --- | --- | --------------- | +// | | click | YES | +// | | input / change | YES | function shouldTriggerChangeEvent(targetDom, evtName) { const { type } = targetDom; const domTag = getDomTag(targetDom); diff --git a/libs/horizon/src/event/utils.ts b/libs/horizon/src/event/utils.ts index 09d1b240..37ec6218 100644 --- a/libs/horizon/src/event/utils.ts +++ b/libs/horizon/src/event/utils.ts @@ -1,9 +1,7 @@ export function isInputElement(dom?: HTMLElement): boolean { - if (dom instanceof HTMLInputElement || dom instanceof HTMLTextAreaElement) { - return true; - } - return false; + return dom instanceof HTMLInputElement || dom instanceof HTMLTextAreaElement; + } export function setPropertyWritable(obj, propName) { diff --git a/libs/horizon/src/renderer/TreeBuilder.ts b/libs/horizon/src/renderer/TreeBuilder.ts index 12626215..32bc6c14 100644 --- a/libs/horizon/src/renderer/TreeBuilder.ts +++ b/libs/horizon/src/renderer/TreeBuilder.ts @@ -43,6 +43,11 @@ let unrecoverableErrorDuringBuild: any = null; // 当前运行的vNode节点 let processing: VNode | null = null; +let currentRoot: VNode | null = null; +export function getCurrentRoot() { + return currentRoot; +} + export function setProcessing(vNode: VNode | null) { processing = vNode; } @@ -267,7 +272,7 @@ function buildVNodeTree(treeRoot: VNode) { // 总体任务入口 function renderFromRoot(treeRoot) { runAsyncEffects(); - + currentRoot = treeRoot; // 1. 构建vNode树 buildVNodeTree(treeRoot); @@ -278,6 +283,7 @@ function renderFromRoot(treeRoot) { // 2. 提交变更 submitToRender(treeRoot); + currentRoot = null; if (window.__HORIZON_DEV_HOOK__) { const hook = window.__HORIZON_DEV_HOOK__; diff --git a/libs/horizon/src/renderer/submit/Submit.ts b/libs/horizon/src/renderer/submit/Submit.ts index e80f911a..d19dfea2 100644 --- a/libs/horizon/src/renderer/submit/Submit.ts +++ b/libs/horizon/src/renderer/submit/Submit.ts @@ -114,21 +114,21 @@ function submit(dirtyNodes: Array) { if ((node.flags & ResetText) === ResetText) { submitResetTextContent(node); } - + if ((node.flags & Ref) === Ref) { if (!node.isCreated) { // 需要执行 detachRef(node, true); } } - + isAdd = (node.flags & Addition) === Addition; isUpdate = (node.flags & Update) === Update; if (isAdd && isUpdate) { // Addition submitAddition(node); FlagUtils.removeFlag(node, Addition); - + // Update submitUpdate(node); } else { @@ -161,7 +161,7 @@ function afterSubmit(dirtyNodes: Array) { if ((node.flags & Update) === Update || (node.flags & Callback) === Callback) { callAfterSubmitLifeCycles(node); } - + if ((node.flags & Ref) === Ref) { attachRef(node); } diff --git a/libs/horizon/src/renderer/vnode/VNode.ts b/libs/horizon/src/renderer/vnode/VNode.ts index d3e6c23a..d67f0e73 100644 --- a/libs/horizon/src/renderer/vnode/VNode.ts +++ b/libs/horizon/src/renderer/vnode/VNode.ts @@ -74,7 +74,10 @@ export class VNode { suspenseState: SuspenseState; path = ''; // 保存从根到本节点的路径 + + // 根节点数据 toUpdateNodes: Set | null; // 保存要更新的节点 + delegatedEvents: Set belongClassVNode: VNode | null = null; // 记录JSXElement所属class vNode,处理ref的时候使用 @@ -94,6 +97,7 @@ export class VNode { this.realNode = realNode; this.task = null; this.toUpdateNodes = new Set(); + this.delegatedEvents = new Set(); this.updates = null; this.stateCallbacks = null; this.state = null; From 9802399b1f72b910730df3b972e191be3dfc527f Mon Sep 17 00:00:00 2001 From: * <8> Date: Tue, 21 Jun 2022 17:50:27 +0800 Subject: [PATCH 03/13] Match-id-ff887de05a36a9d53176b27c20249d9d7588d755 --- libs/horizon/src/dom/DOMOperator.ts | 2 +- .../DOMPropertiesHandler.ts | 34 ++--- libs/horizon/src/dom/utils/Common.ts | 4 + .../src/dom/valueHandler/InputValueHandler.ts | 55 ++++---- .../dom/valueHandler/ValueChangeHandler.ts | 2 +- libs/horizon/src/dom/valueHandler/index.ts | 20 --- .../src/event/ControlledValueUpdater.ts | 37 ------ libs/horizon/src/event/HorizonEventMain.ts | 34 +++-- libs/horizon/src/event/const.ts | 1 + .../simulatedEvtHandler/ChangeEventHandler.ts | 11 +- .../SelectionEventHandler.ts | 112 ---------------- package.json | 1 + scripts/__tests__/EventTest/EventMain.test.js | 120 +++++++++++++++--- scripts/template.ejs | 4 + 14 files changed, 171 insertions(+), 266 deletions(-) delete mode 100644 libs/horizon/src/event/ControlledValueUpdater.ts delete mode 100644 libs/horizon/src/event/simulatedEvtHandler/SelectionEventHandler.ts diff --git a/libs/horizon/src/dom/DOMOperator.ts b/libs/horizon/src/dom/DOMOperator.ts index 073456fc..07700dd4 100644 --- a/libs/horizon/src/dom/DOMOperator.ts +++ b/libs/horizon/src/dom/DOMOperator.ts @@ -26,7 +26,7 @@ import { watchValueChange } from './valueHandler/ValueChangeHandler'; import { DomComponent, DomText } from '../renderer/vnode/VNodeTags'; import { updateCommonProp } from './DOMPropertiesHandler/UpdateCommonProp'; -export type Props = { +export type Props = Record & { autoFocus?: boolean; children?: any; dangerouslySetInnerHTML?: any; diff --git a/libs/horizon/src/dom/DOMPropertiesHandler/DOMPropertiesHandler.ts b/libs/horizon/src/dom/DOMPropertiesHandler/DOMPropertiesHandler.ts index 1ac626a0..0d12dd84 100644 --- a/libs/horizon/src/dom/DOMPropertiesHandler/DOMPropertiesHandler.ts +++ b/libs/horizon/src/dom/DOMPropertiesHandler/DOMPropertiesHandler.ts @@ -1,22 +1,12 @@ -import { - allDelegatedHorizonEvents, -} from '../../event/EventCollection'; +import { allDelegatedHorizonEvents } from '../../event/EventCollection'; import { updateCommonProp } from './UpdateCommonProp'; import { setStyles } from './StyleHandler'; -import { - lazyDelegateOnRoot, - listenNonDelegatedEvent, -} from '../../event/EventBinding'; +import { lazyDelegateOnRoot, listenNonDelegatedEvent } from '../../event/EventBinding'; import { isEventProp } from '../validators/ValidateProps'; import { getCurrentRoot } from '../../renderer/TreeBuilder'; // 初始化DOM属性和更新 DOM 属性 -export function setDomProps( - dom: Element, - props: Object, - isNativeTag: boolean, - isInit: boolean, -): void { +export function setDomProps(dom: Element, props: Object, isNativeTag: boolean, isInit: boolean): void { const keysOfProps = Object.keys(props); let propName; let propVal; @@ -36,7 +26,8 @@ export function setDomProps( } else if (currentRoot && !currentRoot.delegatedEvents.has(propName)) { lazyDelegateOnRoot(currentRoot, propName); } - } else if (propName === 'children') { // 只处理纯文本子节点,其他children在VNode树中处理 + } else if (propName === 'children') { + // 只处理纯文本子节点,其他children在VNode树中处理 const type = typeof propVal; if (type === 'string' || type === 'number') { dom.textContent = propVal; @@ -50,10 +41,7 @@ export function setDomProps( } // 找出两个 DOM 属性的差别,生成需要更新的属性集合 -export function compareProps( - oldProps: Object, - newProps: Object, -): Object { +export function compareProps(oldProps: Object, newProps: Object): Object { let updatesForStyle = {}; const toUpdateProps = {}; const keysOfOldProps = Object.keys(oldProps); @@ -113,7 +101,8 @@ export function compareProps( } if (propName === 'style') { - if (oldPropValue) { // 之前 style 属性有设置非空值 + if (oldPropValue) { + // 之前 style 属性有设置非空值 // 原来有这个 style,但现在没这个 style 了 oldStyleProps = Object.keys(oldPropValue); for (let j = 0; j < oldStyleProps.length; j++) { @@ -131,7 +120,8 @@ export function compareProps( updatesForStyle[styleProp] = newPropValue[styleProp]; } } - } else { // 之前未设置 style 属性或者设置了空值 + } else { + // 之前未设置 style 属性或者设置了空值 if (Object.keys(updatesForStyle).length === 0) { toUpdateProps[propName] = null; } @@ -150,10 +140,12 @@ export function compareProps( toUpdateProps[propName] = String(newPropValue); } } else if (isEventProp(propName)) { + const currentRoot = getCurrentRoot(); if (!allDelegatedHorizonEvents.has(propName)) { toUpdateProps[propName] = newPropValue; + } else if (currentRoot && !currentRoot.delegatedEvents.has(propName)) { + lazyDelegateOnRoot(currentRoot, propName); } - // TODO } else { toUpdateProps[propName] = newPropValue; } diff --git a/libs/horizon/src/dom/utils/Common.ts b/libs/horizon/src/dom/utils/Common.ts index dae06d2b..3ed6a462 100644 --- a/libs/horizon/src/dom/utils/Common.ts +++ b/libs/horizon/src/dom/utils/Common.ts @@ -56,6 +56,10 @@ export function getDomTag(dom) { return dom.nodeName.toLowerCase(); } +export function isInputElement(dom: Element): dom is HTMLInputElement { + return getDomTag(dom) === 'input'; +} + const types = ['button', 'input', 'select', 'textarea']; // button、input、select、textarea、如果有 autoFocus 属性需要focus diff --git a/libs/horizon/src/dom/valueHandler/InputValueHandler.ts b/libs/horizon/src/dom/valueHandler/InputValueHandler.ts index 368bf288..2ebe01a7 100644 --- a/libs/horizon/src/dom/valueHandler/InputValueHandler.ts +++ b/libs/horizon/src/dom/valueHandler/InputValueHandler.ts @@ -1,20 +1,21 @@ -import {updateCommonProp} from '../DOMPropertiesHandler/UpdateCommonProp'; -import {getVNodeProps} from '../DOMInternalKeys'; -import {IProperty} from '../utils/Interface'; -import {isInputValueChanged} from './ValueChangeHandler'; +import { updateCommonProp } from '../DOMPropertiesHandler/UpdateCommonProp'; +import { IProperty } from '../utils/Interface'; +import { isInputElement } from '../utils/Common'; +import { getVNodeProps } from '../DOMInternalKeys'; +import { updateInputValueIfChanged } from './ValueChangeHandler'; function getInitValue(dom: HTMLInputElement, properties: IProperty) { - const {value, defaultValue, checked, defaultChecked} = properties; + const { value, defaultValue, checked, defaultChecked } = properties; const defaultValueStr = defaultValue != null ? defaultValue : ''; const initValue = value != null ? value : defaultValueStr; const initChecked = checked != null ? checked : defaultChecked; - return {initValue, initChecked}; + return { initValue, initChecked }; } export function getInputPropsWithoutValue(dom: HTMLInputElement, properties: IProperty) { - // checked属于必填属性,无法置空 + // checked属于必填属性,无法置 let {checked} = properties; if (checked == null) { checked = getInitValue(dom, properties).initChecked; @@ -59,30 +60,26 @@ export function setInitInputValue(dom: HTMLInputElement, properties: IProperty) dom.defaultChecked = Boolean(initChecked); } -export function resetInputValue(dom: HTMLInputElement, properties: IProperty) { - const {name, type} = properties; - // 如果是 radio,先更新相同 name 的 radio - if (type === 'radio' && name != null) { - const radioList = document.querySelectorAll(`input[type="radio"][name="${name}"]`); +// 找出同一form内,name相同的Radio,更新它们Handler的Value +export function syncRadiosHandler(targetRadio: Element) { + if (isInputElement(targetRadio)) { + const props = getVNodeProps(targetRadio); + if (props) { + const { name, type } = props; + if (type === 'radio' && name != null) { + const radioList = document.querySelectorAll(`input[type="radio"][name="${name}"]`); + for (let i = 0; i < radioList.length; i++) { + const radio = radioList[i]; + if (radio === targetRadio) { + continue; + } + if (radio.form != null && targetRadio.form != null && radio.form !== targetRadio.form) { + continue; + } - for (let i = 0; i < radioList.length; i++) { - const radio = radioList[i]; - if (radio === dom) { - continue; + updateInputValueIfChanged(radio); + } } - // @ts-ignore - if (radio.form !== dom.form) { - continue; - } - - // @ts-ignore - const nonHorizonRadioProps = getVNodeProps(radio); - - isInputValueChanged(radio); - // @ts-ignore - updateInputValue(radio, nonHorizonRadioProps); } - } else { - updateInputValue(dom, properties); } } diff --git a/libs/horizon/src/dom/valueHandler/ValueChangeHandler.ts b/libs/horizon/src/dom/valueHandler/ValueChangeHandler.ts index 34406ef8..207e8c2e 100644 --- a/libs/horizon/src/dom/valueHandler/ValueChangeHandler.ts +++ b/libs/horizon/src/dom/valueHandler/ValueChangeHandler.ts @@ -54,7 +54,7 @@ export function watchValueChange(dom) { } } -export function isInputValueChanged(dom) { +export function updateInputValueIfChanged(dom) { const handler = dom[HANDLER_KEY]; if (!handler) { return true; diff --git a/libs/horizon/src/dom/valueHandler/index.ts b/libs/horizon/src/dom/valueHandler/index.ts index 2e5b6da7..5948a937 100644 --- a/libs/horizon/src/dom/valueHandler/index.ts +++ b/libs/horizon/src/dom/valueHandler/index.ts @@ -8,7 +8,6 @@ import { getInputPropsWithoutValue, setInitInputValue, updateInputValue, - resetInputValue, } from './InputValueHandler'; import { getOptionPropsWithoutValue, @@ -21,7 +20,6 @@ import { getTextareaPropsWithoutValue, updateTextareaValue, } from './TextareaValueHandler'; -import {getDomTag} from '../utils/Common'; // 获取元素除了被代理的值以外的属性 function getPropsWithoutValue(type: string, dom: HorizonDom, properties: IProperty) { @@ -73,26 +71,8 @@ function updateValue(type: string, dom: HorizonDom, properties: IProperty) { } } -function resetValue(dom: HorizonDom, properties: IProperty) { - const type = getDomTag(dom); - switch (type) { - case 'input': - resetInputValue(dom, properties); - break; - case 'select': - updateSelectValue(dom, properties); - break; - case 'textarea': - updateTextareaValue(dom, properties); - break; - default: - break; - } -} - export { getPropsWithoutValue, setInitValue, updateValue, - resetValue, }; diff --git a/libs/horizon/src/event/ControlledValueUpdater.ts b/libs/horizon/src/event/ControlledValueUpdater.ts deleted file mode 100644 index 5e565a1d..00000000 --- a/libs/horizon/src/event/ControlledValueUpdater.ts +++ /dev/null @@ -1,37 +0,0 @@ -import {getVNodeProps} from '../dom/DOMInternalKeys'; -import {resetValue} from '../dom/valueHandler'; - -let updateList: Array | null = null; - -// 受控组件值重新赋值 -function updateValue(target: Element) { - const props = getVNodeProps(target); - if (props) { - resetValue(target, props); - } -} - -// 存储队列中缓存组件 -export function addValueUpdateList(target: EventTarget): void { - if (updateList) { - updateList.push(target); - } else { - updateList = [target]; - } -} - -// 判断是否需要重新赋值 -export function shouldUpdateValue(): boolean { - return updateList !== null && updateList.length > 0; -} - -// 从缓存队列中对受控组件进行赋值 -export function updateControlledValue() { - if (!updateList) { - return; - } - updateList.forEach(item => { - updateValue(item); - }); - updateList = null; -} diff --git a/libs/horizon/src/event/HorizonEventMain.ts b/libs/horizon/src/event/HorizonEventMain.ts index 35db487b..e183ce2f 100644 --- a/libs/horizon/src/event/HorizonEventMain.ts +++ b/libs/horizon/src/event/HorizonEventMain.ts @@ -1,22 +1,15 @@ import type { AnyNativeEvent } from './Types'; +import { ListenerUnitList } from './Types'; import type { VNode } from '../renderer/Types'; -import { - CommonEventToHorizonMap, - horizonEventToNativeMap, - EVENT_TYPE_BUBBLE, - EVENT_TYPE_CAPTURE, -} from './const'; +import { CommonEventToHorizonMap, EVENT_TYPE_BUBBLE, EVENT_TYPE_CAPTURE, horizonEventToNativeMap } from './const'; import { getListeners as getChangeListeners } from './simulatedEvtHandler/ChangeEventHandler'; -import { - setPropertyWritable, -} from './utils'; +import { setPropertyWritable } from './utils'; import { decorateNativeEvent } from './customEvents/EventFactory'; import { getListenersFromTree } from './ListenerGetter'; -import { shouldUpdateValue, updateControlledValue } from './ControlledValueUpdater'; import { asyncUpdates, runDiscreteUpdates } from '../renderer/Renderer'; import { findRoot } from '../renderer/vnode/VNodeUtils'; -import { ListenerUnitList } from './Types'; +import { syncRadiosHandler } from '../dom/valueHandler/InputValueHandler'; // web规范,鼠标右键key值 const RIGHT_MOUSE_BUTTON = 2; @@ -80,7 +73,6 @@ function getProcessListeners( target, isCapture: boolean, ): ListenerUnitList { - // TODO 重复从树中获取监听器 // 触发普通委托事件 let listenerList: ListenerUnitList = getCommonListeners( nativeEvtName, @@ -92,12 +84,11 @@ function getProcessListeners( // 触发特殊handler委托事件 if (!isCapture) { - if (horizonEventToNativeMap.get('onChange').includes(nativeEvtName)) { + if (horizonEventToNativeMap.get('onChange')!.includes(nativeEvtName)) { listenerList = listenerList.concat(getChangeListeners( nativeEvtName, nativeEvent, vNode, - target, )); } } @@ -110,7 +101,7 @@ function triggerHorizonEvents( isCapture: boolean, nativeEvent: AnyNativeEvent, vNode: VNode | null, -): void { +) { const nativeEventTarget = nativeEvent.target || nativeEvent.srcElement; // 获取委托事件队列 @@ -118,6 +109,8 @@ function triggerHorizonEvents( // 处理触发的事件队列 processListeners(listenerList); + + return listenerList; } @@ -148,13 +141,18 @@ export function handleEventMain( // 没有事件在执行,经过调度再执行事件 isInEventsExecution = true; + let shouldDispatchUpdate = false; try { - asyncUpdates(() => triggerHorizonEvents(nativeEvtName, isCapture, nativeEvent, startVNode)); + const listeners = asyncUpdates(() => triggerHorizonEvents(nativeEvtName, isCapture, nativeEvent, startVNode)); + if (listeners.length) { + shouldDispatchUpdate = true; + } } finally { isInEventsExecution = false; - if (shouldUpdateValue()) { + if (shouldDispatchUpdate) { runDiscreteUpdates(); - updateControlledValue(); + // 若是Radio,同步同组其他Radio的Handler Value + syncRadiosHandler(nativeEvent.target as Element); } } } diff --git a/libs/horizon/src/event/const.ts b/libs/horizon/src/event/const.ts index c0e185a4..87d10821 100644 --- a/libs/horizon/src/event/const.ts +++ b/libs/horizon/src/event/const.ts @@ -44,6 +44,7 @@ export const CommonEventToHorizonMap = { focusin: 'focus', focusout: 'blur', input: 'input', + select: 'select', keydown: 'keyDown', keypress: 'keyPress', keyup: 'keyUp', diff --git a/libs/horizon/src/event/simulatedEvtHandler/ChangeEventHandler.ts b/libs/horizon/src/event/simulatedEvtHandler/ChangeEventHandler.ts index f1b781af..285ac435 100644 --- a/libs/horizon/src/event/simulatedEvtHandler/ChangeEventHandler.ts +++ b/libs/horizon/src/event/simulatedEvtHandler/ChangeEventHandler.ts @@ -1,7 +1,6 @@ import {decorateNativeEvent} from '../customEvents/EventFactory'; import {getDom} from '../../dom/DOMInternalKeys'; -import {isInputValueChanged} from '../../dom/valueHandler/ValueChangeHandler'; -import {addValueUpdateList} from '../ControlledValueUpdater'; +import {updateInputValueIfChanged} from '../../dom/valueHandler/ValueChangeHandler'; import {isInputElement} from '../utils'; import {EVENT_TYPE_ALL} from '../const'; import {AnyNativeEvent, ListenerUnitList} from '../Types'; @@ -25,11 +24,11 @@ function shouldTriggerChangeEvent(targetDom, evtName) { return evtName === 'change'; } else if (domTag === 'input' && (type === 'checkbox' || type === 'radio')) { if (evtName === 'click') { - return isInputValueChanged(targetDom); + return updateInputValueIfChanged(targetDom); } } else if (isInputElement(targetDom)) { if (evtName === 'input' || evtName === 'change') { - return isInputValueChanged(targetDom); + return updateInputValueIfChanged(targetDom); } } return false; @@ -42,8 +41,7 @@ function shouldTriggerChangeEvent(targetDom, evtName) { export function getListeners( nativeEvtName: string, nativeEvt: AnyNativeEvent, - vNode: null | VNode, - target: null | EventTarget, + vNode: null | VNode ): ListenerUnitList { if (!vNode) { return []; @@ -52,7 +50,6 @@ export function getListeners( // 判断是否需要触发change事件 if (shouldTriggerChangeEvent(targetDom, nativeEvtName)) { - addValueUpdateList(target); const event = decorateNativeEvent( 'onChange', 'change', diff --git a/libs/horizon/src/event/simulatedEvtHandler/SelectionEventHandler.ts b/libs/horizon/src/event/simulatedEvtHandler/SelectionEventHandler.ts deleted file mode 100644 index cd5a09ad..00000000 --- a/libs/horizon/src/event/simulatedEvtHandler/SelectionEventHandler.ts +++ /dev/null @@ -1,112 +0,0 @@ -import {decorateNativeEvent} from '../customEvents/EventFactory'; -import {shallowCompare} from '../../renderer/utils/compare'; -import {getFocusedDom} from '../../dom/utils/Common'; -import {getDom} from '../../dom/DOMInternalKeys'; -import {isDocument} from '../../dom/utils/Common'; -import {isInputElement, setPropertyWritable} from '../utils'; -import type {AnyNativeEvent} from '../Types'; -import {getListenersFromTree} from '../ListenerGetter'; -import type {VNode} from '../../renderer/Types'; -import {EVENT_TYPE_ALL} from '../const'; -import {ListenerUnitList} from '../Types'; - -const horizonEventName = 'onSelect'; - -let currentElement = null; -let currentVNode = null; -let lastSelection: Selection | null = null; - -function initTargetCache(dom, vNode) { - if (isInputElement(dom) || dom.contentEditable === 'true') { - currentElement = dom; - currentVNode = vNode; - lastSelection = null; - } -} - -function clearTargetCache() { - currentElement = null; - currentVNode = null; - lastSelection = null; -} - -// 标记是否在鼠标事件过程中 -let isInMouseEvent = false; - -// 获取节点所在的document对象 -function getDocument(eventTarget) { - if (eventTarget.window === eventTarget) { - return eventTarget.document; - } - if (isDocument(eventTarget)) { - return eventTarget; - } - return eventTarget.ownerDocument; -} - -function getSelectEvent(nativeEvent, target) { - const doc = getDocument(target); - if (isInMouseEvent || currentElement == null || currentElement !== getFocusedDom(doc)) { - return []; - } - - const currentSelection = window.getSelection(); - if (!shallowCompare(lastSelection, currentSelection)) { - lastSelection = currentSelection; - - const event = decorateNativeEvent( - horizonEventName, - 'select', - nativeEvent, - ); - setPropertyWritable(nativeEvent, 'target'); - event.target = currentElement; - - return getListenersFromTree( - currentVNode, - horizonEventName, - event, - EVENT_TYPE_ALL - ); - } - return []; -} - - -/** - * 该插件创建一个onSelect事件 - * 支持元素: input、textarea、contentEditable元素 - * 触发场景:用户输入、折叠选择、文本选择 - */ -export function getListeners( - nativeEvtName: string, - nativeEvt: AnyNativeEvent, - vNode: null | VNode, - target: null | EventTarget, -): ListenerUnitList { - const targetNode = vNode ? getDom(vNode) : window; - let eventUnitList: ListenerUnitList = []; - switch (nativeEvtName) { - case 'focusin': - initTargetCache(targetNode, vNode); - break; - case 'focusout': - clearTargetCache(); - break; - case 'mousedown': - isInMouseEvent = true; - break; - case 'contextmenu': - case 'mouseup': - case 'dragend': - isInMouseEvent = false; - eventUnitList = getSelectEvent(nativeEvt, target); - break; - case 'selectionchange': - case 'keydown': - case 'keyup': - eventUnitList = getSelectEvent(nativeEvt, target); - } - - return eventUnitList; -} diff --git a/package.json b/package.json index 58bf2f9f..c31b27cf 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "scripts": { "lint": "eslint . --ext .ts", "build": " rollup --config ./scripts/rollup/rollup.config.js", + "build:watch": " rollup --watch --config ./scripts/rollup/rollup.config.js", "build-3rdLib": "node ./scripts/gen3rdLib.js", "build-3rdLib-dev": "npm run build & node ./scripts/gen3rdLib.js --dev", "build-horizon3rdLib-dev": "npm run build & node ./scripts/gen3rdLib.js --dev --type horizon", diff --git a/scripts/__tests__/EventTest/EventMain.test.js b/scripts/__tests__/EventTest/EventMain.test.js index 4a75d46e..1e6c52b7 100644 --- a/scripts/__tests__/EventTest/EventMain.test.js +++ b/scripts/__tests__/EventTest/EventMain.test.js @@ -34,7 +34,7 @@ describe('事件', () => { 'btn capture', 'btn bubble', 'p bubble', - 'div bubble' + 'div bubble', ]); }); @@ -46,14 +46,14 @@ describe('事件', () => { keyCode = e.keyCode; }} />, - container, + container ); node.dispatchEvent( new KeyboardEvent('keypress', { keyCode: 65, bubbles: true, cancelable: true, - }), + }) ); expect(keyCode).toBe(65); }); @@ -64,7 +64,10 @@ describe('事件', () => { <>
LogUtils.log('div capture')} onClick={() => LogUtils.log('div bubble')}>

LogUtils.log('p capture')} onClick={() => LogUtils.log('p bubble')}> -

@@ -78,7 +81,7 @@ describe('事件', () => { 'div capture', 'p capture', 'btn capture', - 'btn bubble' + 'btn bubble', ]); }); @@ -86,7 +89,10 @@ describe('事件', () => { const App = () => { return ( <> -
TestUtils.stopBubbleOrCapture(e, 'div capture')} onClick={() => LogUtils.log('div bubble')}> +
TestUtils.stopBubbleOrCapture(e, 'div capture')} + onClick={() => LogUtils.log('div bubble')} + >

LogUtils.log('p capture')} onClick={() => LogUtils.log('p bubble')}> + +

{props.children}
+
+ ); + } + + it('测试Array方法: push()、pop()', () => { + function Child(props) { + const userStore = useStore('user'); + + return ( +
+ +
+ ); + } + + Horizon.render(, container); + + expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: 2'); + // 在Array中增加一个对象 + Horizon.act(() => { + triggerClickEvent(container, 'addBtn'); + }); + expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: 3'); + + // 在Array中删除一个对象 + Horizon.act(() => { + triggerClickEvent(container, 'delBtn'); + }); + expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: 2'); + }); + + it('测试Array方法: entries()、push()、shift()、unshift、直接赋值', () => { + let globalStore = null; + + function Child(props) { + const userStore = useStore('user'); + globalStore = userStore; + + const nameList = []; + const entries = userStore.$state.persons?.entries(); + if (entries) { + for (const entry of entries) { + nameList.push(entry[1].name); + } + } + + return ( +
+ +
+ ); + } + + Horizon.render(, container); + + expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2'); + // push + globalStore.$state.persons.push(newPerson); + expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3'); + + // shift + globalStore.$state.persons.shift({ name: 'p0', age: 0 }); + expect(container.querySelector('#nameList').innerHTML).toBe('name list: p2 p3'); + + // 赋值[2] + globalStore.$state.persons[2] = { name: 'p4', age: 4 }; + expect(container.querySelector('#nameList').innerHTML).toBe('name list: p2 p3 p4'); + + // 重新赋值[2] + globalStore.$state.persons[2] = { name: 'p5', age: 5 }; + expect(container.querySelector('#nameList').innerHTML).toBe('name list: p2 p3 p5'); + + // unshift + globalStore.$state.persons.unshift({ name: 'p1', age: 1 }); + expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3 p5'); + + // 重新赋值 null + globalStore.$state.persons = null; + expect(container.querySelector('#nameList').innerHTML).toBe('name list: '); + + // 重新赋值 [{ name: 'p1', age: 1 }] + globalStore.$state.persons = [{ name: 'p1', age: 1 }]; + expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1'); + }); + + it('测试Array方法: forEach()', () => { + let globalStore = null; + + function Child(props) { + const userStore = useStore('user'); + globalStore = userStore; + + const nameList = []; + userStore.$state.persons?.forEach(per => { + nameList.push(per.name); + }); + + return ( +
+ +
+ ); + } + + Horizon.render(, container); + + expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2'); + // push + globalStore.$state.persons.push(newPerson); + expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3'); + + // shift + globalStore.$state.persons.shift({ name: 'p0', age: 0 }); + expect(container.querySelector('#nameList').innerHTML).toBe('name list: p2 p3'); + + // 赋值[2] + globalStore.$state.persons[2] = { name: 'p4', age: 4 }; + expect(container.querySelector('#nameList').innerHTML).toBe('name list: p2 p3 p4'); + + // 重新赋值[2] + globalStore.$state.persons[2] = { name: 'p5', age: 5 }; + expect(container.querySelector('#nameList').innerHTML).toBe('name list: p2 p3 p5'); + + // unshift + globalStore.$state.persons.unshift({ name: 'p1', age: 1 }); + expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3 p5'); + + // 重新赋值 null + globalStore.$state.persons = null; + expect(container.querySelector('#nameList').innerHTML).toBe('name list: '); + + // 重新赋值 [{ name: 'p1', age: 1 }] + globalStore.$state.persons = [{ name: 'p1', age: 1 }]; + expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1'); + }); +}); diff --git a/scripts/__tests__/HorizonXText/StateManager/StateMap.test.js b/scripts/__tests__/HorizonXText/StateManager/StateMap.test.js new file mode 100644 index 00000000..f7eaed9f --- /dev/null +++ b/scripts/__tests__/HorizonXText/StateManager/StateMap.test.js @@ -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 ( +
+ + + +
{props.children}
+
+ ); + } + + it('测试Map方法: set()、delete()、clear()', () => { + function Child(props) { + const userStore = useStore('user'); + + return ( +
+ +
+ ); + } + + Horizon.render(, 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 ( +
+ +
+ ); + } + + Horizon.render(, 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 ( +
+ +
+ ); + } + + Horizon.render(, 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 ( +
+ +
+ ); + } + + Horizon.render(, 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 ( +
+ +
+ ); + } + + Horizon.render(, 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 ( +
+ +
+ ); + } + + Horizon.render(, 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 ( +
+ +
+ ); + } + + Horizon.render(, 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: '); + }); +}); diff --git a/scripts/__tests__/HorizonXText/StateManager/StateMixType.test.js b/scripts/__tests__/HorizonXText/StateManager/StateMixType.test.js new file mode 100644 index 00000000..870d7d26 --- /dev/null +++ b/scripts/__tests__/HorizonXText/StateManager/StateMixType.test.js @@ -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 ( +
+ +
{props.children}
+
+ ); + } + + 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 ( +
+ +
+ ); + } + + Horizon.render(, 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 ( +
+ +
+ ); + } + + Horizon.render(, 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'); + }); +}); diff --git a/scripts/__tests__/HorizonXText/StateManager/StateSet.test.js b/scripts/__tests__/HorizonXText/StateManager/StateSet.test.js new file mode 100644 index 00000000..4b6bdc7b --- /dev/null +++ b/scripts/__tests__/HorizonXText/StateManager/StateSet.test.js @@ -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 ( +
+ + + +
{props.children}
+
+ ); + } + + 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 ( +
+ + +
+ ); + } + + Horizon.render(, 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 ( +
+ +
+ ); + } + + Horizon.render(, 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 ( +
+ +
+ ); + } + + Horizon.render(, 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 ( +
+ +
+ ); + } + + Horizon.render(, 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 ( +
+ +
+ ); + } + + Horizon.render(, 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 ( +
+ +
+ ); + } + + Horizon.render(, 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: '); + }); +}); diff --git a/scripts/__tests__/HorizonXText/StateManager/StateWeakMap.test.js b/scripts/__tests__/HorizonXText/StateManager/StateWeakMap.test.js new file mode 100644 index 00000000..28d4ff98 --- /dev/null +++ b/scripts/__tests__/HorizonXText/StateManager/StateWeakMap.test.js @@ -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 ( +
+ + + +
{props.children}
+
+ ); + } + + it('测试WeakMap方法: set()、delete()、has()', () => { + function Child(props) { + const userStore = useStore('user'); + + return ( +
+ +
+ ); + } + + Horizon.render(, 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 ( +
+ +
+ ); + } + + Horizon.render(, 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'); + }); +}); diff --git a/scripts/__tests__/HorizonXText/StateManager/StateWeakSet.test.js b/scripts/__tests__/HorizonXText/StateManager/StateWeakSet.test.js new file mode 100644 index 00000000..4c6a8fca --- /dev/null +++ b/scripts/__tests__/HorizonXText/StateManager/StateWeakSet.test.js @@ -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 ( +
+ + +
{props.children}
+
+ ); + } + + it('测试WeakSet方法: add()、delete()、has()', () => { + function Child(props) { + const userStore = useStore('user'); + + return ( +
+ +
+ ); + } + + Horizon.render(, 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'); + }); +}); diff --git a/scripts/__tests__/HorizonXText/StoreFunctionality/async.test.js b/scripts/__tests__/HorizonXText/StoreFunctionality/async.test.js new file mode 100644 index 00000000..f45599d2 --- /dev/null +++ b/scripts/__tests__/HorizonXText/StoreFunctionality/async.test.js @@ -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 ( +
+

{store.value}

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

{store.value}

+
+ ); + } + + Horizon.render(, 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); + }); +}); diff --git a/scripts/__tests__/HorizonXText/StoreFunctionality/basicAccess.test.js b/scripts/__tests__/HorizonXText/StoreFunctionality/basicAccess.test.js new file mode 100644 index 00000000..e42feeda --- /dev/null +++ b/scripts/__tests__/HorizonXText/StoreFunctionality/basicAccess.test.js @@ -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
{logStore.length}
; + } + + Horizon.render(, container); + + expect(document.getElementById(RESULT_ID).innerHTML).toBe('1'); + }); + + it('Should use actions and update components', () => { + function App() { + const logStore = useLogStore(); + + return ( +
+ +

{logStore.length}

+
+ ); + } + + Horizon.render(, container); + + Horizon.act(() => { + triggerClickEvent(container, BUTTON_ID); + }); + + expect(document.getElementById(RESULT_ID).innerHTML).toBe('2'); + }); +}); diff --git a/scripts/__tests__/HorizonXText/StoreFunctionality/dollarAccess.test.js b/scripts/__tests__/HorizonXText/StoreFunctionality/dollarAccess.test.js new file mode 100644 index 00000000..032234d0 --- /dev/null +++ b/scripts/__tests__/HorizonXText/StoreFunctionality/dollarAccess.test.js @@ -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
{logStore.$computed.length()}
; + } + + Horizon.render(, container); + + expect(document.getElementById(RESULT_ID).innerHTML).toBe('1'); + }); + + it('Should use $actions and update components', () => { + function App() { + const logStore = useLogStore(); + + return ( +
+ +

{logStore.$computed.length()}

+
+ ); + } + + Horizon.render(, container); + + Horizon.act(() => { + triggerClickEvent(container, BUTTON_ID); + }); + + expect(document.getElementById(RESULT_ID).innerHTML).toBe('2'); + }); +}); diff --git a/scripts/__tests__/HorizonXText/StoreFunctionality/otherCases.test.js b/scripts/__tests__/HorizonXText/StoreFunctionality/otherCases.test.js new file mode 100644 index 00000000..115164c0 --- /dev/null +++ b/scripts/__tests__/HorizonXText/StoreFunctionality/otherCases.test.js @@ -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 ( +
+

{store.double}

+ +
+ ); + } + + Horizon.render(, 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 ( +
+

{store.magicConstant}

+ +
+ ); + } + + Horizon.render(, 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 ( +
+

{store.getItem(0) + store.getItem(1) + store.getItem(2)}

+ +
+ ); + } + + Horizon.render(, container); + expect(document.getElementById(RESULT_ID).innerHTML).toBe('abc'); + + Horizon.act(() => { + triggerClickEvent(container, BUTTON_ID); + }); + expect(document.getElementById(RESULT_ID).innerHTML).toBe('def'); + }); +}); diff --git a/scripts/__tests__/HorizonXText/StoreFunctionality/reset.test.js b/scripts/__tests__/HorizonXText/StoreFunctionality/reset.test.js new file mode 100644 index 00000000..f0b349da --- /dev/null +++ b/scripts/__tests__/HorizonXText/StoreFunctionality/reset.test.js @@ -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 ( +
+

{store.$state.counter}

+ + +
+ ); + } + + Horizon.render(, 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'); + }); +}); diff --git a/scripts/__tests__/HorizonXText/StoreFunctionality/store.js b/scripts/__tests__/HorizonXText/StoreFunctionality/store.js new file mode 100644 index 00000000..ccdbc6a6 --- /dev/null +++ b/scripts/__tests__/HorizonXText/StoreFunctionality/store.js @@ -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], + }, +}); diff --git a/scripts/__tests__/HorizonXText/adapters/ReduxAdapter.test.js b/scripts/__tests__/HorizonXText/adapters/ReduxAdapter.test.js new file mode 100644 index 00000000..2fa93598 --- /dev/null +++ b/scripts/__tests__/HorizonXText/adapters/ReduxAdapter.test.js @@ -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"); + }); +}); diff --git a/scripts/__tests__/HorizonXText/adapters/ReduxAdapterPromiseMiddleware.js b/scripts/__tests__/HorizonXText/adapters/ReduxAdapterPromiseMiddleware.js new file mode 100644 index 00000000..509706cf --- /dev/null +++ b/scripts/__tests__/HorizonXText/adapters/ReduxAdapterPromiseMiddleware.js @@ -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; + }; + }; +} diff --git a/scripts/__tests__/HorizonXText/adapters/ReduxAdapterThunk.test.js b/scripts/__tests__/HorizonXText/adapters/ReduxAdapterThunk.test.js new file mode 100644 index 00000000..3785b92c --- /dev/null +++ b/scripts/__tests__/HorizonXText/adapters/ReduxAdapterThunk.test.js @@ -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); + }); +}); diff --git a/scripts/__tests__/HorizonXText/adapters/ReduxReactAdapter.test.js b/scripts/__tests__/HorizonXText/adapters/ReduxReactAdapter.test.js new file mode 100644 index 00000000..5977671a --- /dev/null +++ b/scripts/__tests__/HorizonXText/adapters/ReduxReactAdapter.test.js @@ -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
{store.getState()}
; + }; + + const Wrapper = () => { + return ( + + + + ); + }; + + Horizon.render(, 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 ( +
+

{store.getState()}

+ +
+ ); + }; + + const Wrapper = () => { + return ( + + + + ); + }; + + Horizon.render(, 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 ( +
+

{count}

+ +
+ ); + }; + + const Wrapper = () => { + return ( + + + + ); + }; + + Horizon.render(, 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 ( +
+
+ {n ? '-' : '+'} + {props.stateProps.value} +
+ +
+ ); + }); + + const Wrapper = () => { + const [amount, setAmount] = Horizon.useState(5); + return ( + + + + + ); + }; + + Horizon.render(, 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 ( +
+

{value}

+ +
+ ); + } + + Horizon.render( + + + , + 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 ( + + ); + } + + function Toggle() { + const check = createSelectorHook(toggleContext)(); + const dispatch = createDispatchHook(toggleContext)(); + + return ( + + ); + } + + function Wrapper() { + return ( +
+ + + + + + + +
+ ); + } + + Horizon.render(, 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'); + }); +}); diff --git a/scripts/__tests__/HorizonXText/class/ClassException.test.js b/scripts/__tests__/HorizonXText/class/ClassException.test.js new file mode 100644 index 00000000..64cea5aa --- /dev/null +++ b/scripts/__tests__/HorizonXText/class/ClassException.test.js @@ -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 ( +
+ + +
+ ); + } + } + + expect(() => { + Horizon.render(, container); + }).toThrow( + 'The number of updates exceeds the upper limit 50.\n' + + ' A component maybe repeatedly invokes setState on componentWillUpdate or componentDidUpdate.' + ); + }); +}); diff --git a/scripts/__tests__/HorizonXText/class/ClassStateArray.test.js b/scripts/__tests__/HorizonXText/class/ClassStateArray.test.js new file mode 100644 index 00000000..c61aed9c --- /dev/null +++ b/scripts/__tests__/HorizonXText/class/ClassStateArray.test.js @@ -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 ( +
+ + +
{this.props.children}
+
+ ); + } + } + + it('测试Array方法: push()、pop()', () => { + class Child extends Horizon.Component { + userStore = useStore('user'); + + render() { + return ( +
+ +
+ ); + } + } + + Horizon.render(, container); + + expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: 2'); + // 在Array中增加一个对象 + Horizon.act(() => { + triggerClickEvent(container, 'addBtn'); + }); + expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: 3'); + + // 在Array中删除一个对象 + Horizon.act(() => { + triggerClickEvent(container, 'delBtn'); + }); + expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: 2'); + }); + + it('测试Array方法: entries()、push()、shift()、unshift、直接赋值', () => { + let globalStore = null; + + class Child extends Horizon.Component { + userStore = useStore('user'); + + constructor(props) { + super(props); + globalStore = this.userStore; + } + + render() { + const nameList = []; + const entries = this.userStore.$state.persons?.entries(); + if (entries) { + for (const entry of entries) { + nameList.push(entry[1].name); + } + } + + return ( +
+ +
+ ); + } + } + + Horizon.render(, container); + + expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2'); + // push + globalStore.$state.persons.push(newPerson); + expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3'); + + // shift + globalStore.$state.persons.shift({ name: 'p0', age: 0 }); + expect(container.querySelector('#nameList').innerHTML).toBe('name list: p2 p3'); + + // 赋值[2] + globalStore.$state.persons[2] = { name: 'p4', age: 4 }; + expect(container.querySelector('#nameList').innerHTML).toBe('name list: p2 p3 p4'); + + // 重新赋值[2] + globalStore.$state.persons[2] = { name: 'p5', age: 5 }; + expect(container.querySelector('#nameList').innerHTML).toBe('name list: p2 p3 p5'); + + // unshift + globalStore.$state.persons.unshift({ name: 'p1', age: 1 }); + expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3 p5'); + + // 重新赋值 null + globalStore.$state.persons = null; + expect(container.querySelector('#nameList').innerHTML).toBe('name list: '); + + // 重新赋值 [{ name: 'p1', age: 1 }] + globalStore.$state.persons = [{ name: 'p1', age: 1 }]; + expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1'); + }); + + it('测试Array方法: forEach()', () => { + let globalStore = null; + + class Child extends Horizon.Component { + userStore = useStore('user'); + + constructor(props) { + super(props); + globalStore = this.userStore; + } + + render() { + const nameList = []; + this.userStore.$state.persons?.forEach(per => { + nameList.push(per.name); + }); + + return ( +
+ +
+ ); + } + } + + Horizon.render(, container); + + expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2'); + // push + globalStore.$state.persons.push(newPerson); + expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3'); + + // shift + globalStore.$state.persons.shift({ name: 'p0', age: 0 }); + expect(container.querySelector('#nameList').innerHTML).toBe('name list: p2 p3'); + + // 赋值[2] + globalStore.$state.persons[2] = { name: 'p4', age: 4 }; + expect(container.querySelector('#nameList').innerHTML).toBe('name list: p2 p3 p4'); + + // 重新赋值[2] + globalStore.$state.persons[2] = { name: 'p5', age: 5 }; + expect(container.querySelector('#nameList').innerHTML).toBe('name list: p2 p3 p5'); + + // unshift + globalStore.$state.persons.unshift({ name: 'p1', age: 1 }); + expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3 p5'); + + // 重新赋值 null + globalStore.$state.persons = null; + expect(container.querySelector('#nameList').innerHTML).toBe('name list: '); + + // 重新赋值 [{ name: 'p1', age: 1 }] + globalStore.$state.persons = [{ name: 'p1', age: 1 }]; + expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1'); + }); +}); diff --git a/scripts/__tests__/HorizonXText/class/ClassStateMap.test.js b/scripts/__tests__/HorizonXText/class/ClassStateMap.test.js new file mode 100644 index 00000000..071ad650 --- /dev/null +++ b/scripts/__tests__/HorizonXText/class/ClassStateMap.test.js @@ -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 ( +
+ + + +
{this.props.children}
+
+ ); + } + } + + it('测试Map方法: set()、delete()、clear()', () => { + class Child extends Horizon.Component { + userStore = useStore('user'); + + render() { + return ( +
+ +
+ ); + } + } + + Horizon.render(, 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 ( +
+ +
+ ); + } + } + + Horizon.render(, 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 ( +
+ +
+ ); + } + } + + Horizon.render(, 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 ( +
+ +
+ ); + } + } + + Horizon.render(, 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 ( +
+ +
+ ); + } + } + + Horizon.render(, 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 ( +
+ +
+ ); + } + } + + Horizon.render(, 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 ( +
+ +
+ ); + } + } + + Horizon.render(, 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: '); + }); +}); diff --git a/scripts/__tests__/HorizonXText/clear/ClassVNodeClear.test.js b/scripts/__tests__/HorizonXText/clear/ClassVNodeClear.test.js new file mode 100644 index 00000000..a2226b1c --- /dev/null +++ b/scripts/__tests__/HorizonXText/clear/ClassVNodeClear.test.js @@ -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 ( +
+ + {this.userStore.isShow && } +
+ ); + } + } + + class Parent extends Horizon.Component { + userStore = useStore('user'); + + setWin = () => { + this.userStore.setWin(!this.userStore.isWin); + }; + + render() { + return ( +
+ + {this.userStore.isWin && } +
+ ); + } + } + + 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 ( +
+ + +
+ ); + } + } + + Horizon.render(, 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); + }); +}); diff --git a/scripts/__tests__/HorizonXText/clear/FunctionVNodeClear.test.js b/scripts/__tests__/HorizonXText/clear/FunctionVNodeClear.test.js new file mode 100644 index 00000000..ead0453a --- /dev/null +++ b/scripts/__tests__/HorizonXText/clear/FunctionVNodeClear.test.js @@ -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 ( +
+ + {this.userStore.isShow && } +
+ ); + } + } + + class Parent extends Horizon.Component { + userStore = useStore('user'); + + setWin = () => { + this.userStore.setWin(!this.userStore.isWin); + }; + + render() { + return ( +
+ + {this.userStore.isWin && } +
+ ); + } + } + + class Child extends Horizon.Component { + userStore = useStore('user'); + + render() { + return ( +
+ + +
+ ); + } + } + + Horizon.render(, 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); + }); +}); diff --git a/scripts/__tests__/HorizonXText/edgeCases/proxy.test.js b/scripts/__tests__/HorizonXText/edgeCases/proxy.test.js new file mode 100644 index 00000000..a643f311 --- /dev/null +++ b/scripts/__tests__/HorizonXText/edgeCases/proxy.test.js @@ -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); + }); +}); diff --git a/scripts/__tests__/jest/commonComponents.js b/scripts/__tests__/jest/commonComponents.js index 6abda0f8..4823b9b7 100644 --- a/scripts/__tests__/jest/commonComponents.js +++ b/scripts/__tests__/jest/commonComponents.js @@ -1,9 +1,8 @@ - // eslint-disable-next-line @typescript-eslint/no-unused-vars import * as Horizon from '@cloudsop/horizon/index.ts'; import { getLogUtils } from './testUtils'; -export const App = (props) => { +export const App = props => { const Parent = props.parent; const Child = props.child; @@ -16,8 +15,15 @@ export const App = (props) => { ); }; -export const Text = (props) => { - const LogUtils =getLogUtils(); +export const Text = props => { + const LogUtils = getLogUtils(); LogUtils.log(props.text); return

{props.text}

; }; + +export function triggerClickEvent(container, id) { + const event = new MouseEvent('click', { + bubbles: true, + }); + container.querySelector(`#${id}`).dispatchEvent(event); +} diff --git a/scripts/rollup/rollup.config.js b/scripts/rollup/rollup.config.js index fb40e560..4d5cf9be 100644 --- a/scripts/rollup/rollup.config.js +++ b/scripts/rollup/rollup.config.js @@ -1,6 +1,7 @@ import nodeResolve from '@rollup/plugin-node-resolve'; import babel from '@rollup/plugin-babel'; import path from 'path'; +import fs from 'fs'; import replace from '@rollup/plugin-replace'; import copy from './copy-plugin'; import { terser } from 'rollup-plugin-terser'; @@ -11,6 +12,14 @@ const extensions = ['.js', '.ts']; const libDir = path.join(__dirname, '../../libs/horizon'); const rootDir = path.join(__dirname, '../..'); 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); function genConfig(mode) { From e3895bda8bafa0a5bd3497d014676e3f2b7a3dfd Mon Sep 17 00:00:00 2001 From: * <8> Date: Mon, 27 Jun 2022 14:18:16 +0800 Subject: [PATCH 07/13] Match-id-efc4c1e8f6dd5638e77058d13d15814e4d831207 --- libs/horizon/src/event/EventBinding.ts | 5 +- libs/horizon/src/event/HorizonEventMain.ts | 1 - .../ComponentTest/HookTest/UseEffect.test.js | 8 ++- scripts/__tests__/DomTest/DomInput.test.js | 58 ------------------- scripts/__tests__/DomTest/DomSelect.test.js | 33 +---------- scripts/__tests__/DomTest/DomTextarea.test.js | 22 +------ 6 files changed, 11 insertions(+), 116 deletions(-) diff --git a/libs/horizon/src/event/EventBinding.ts b/libs/horizon/src/event/EventBinding.ts index 9398d648..5a38e45b 100644 --- a/libs/horizon/src/event/EventBinding.ts +++ b/libs/horizon/src/event/EventBinding.ts @@ -70,9 +70,10 @@ export function lazyDelegateOnRoot(currentRoot: VNode, eventName: string) { const nativeEvents = allDelegatedHorizonEvents.get(eventName); nativeEvents.forEach(nativeEvent => { - if (!currentRoot.delegatedNativeEvents.has(nativeEvent)) { + const nativeFullName = isCapture ? nativeEvent + 'capture' : nativeEvent; + if (!currentRoot.delegatedNativeEvents.has(nativeFullName)) { listenToNativeEvent(nativeEvent, currentRoot.realNode, isCapture); - currentRoot.delegatedNativeEvents.add(nativeEvent); + currentRoot.delegatedNativeEvents.add(nativeFullName); } }); } diff --git a/libs/horizon/src/event/HorizonEventMain.ts b/libs/horizon/src/event/HorizonEventMain.ts index aff68d17..96fe5167 100644 --- a/libs/horizon/src/event/HorizonEventMain.ts +++ b/libs/horizon/src/event/HorizonEventMain.ts @@ -206,7 +206,6 @@ export function handleEventMain( } finally { isInEventsExecution = false; if (shouldDispatchUpdate) { - runDiscreteUpdates(); // 若是Radio,同步同组其他Radio的Handler Value syncRadiosHandler(nativeEvent.target as Element); } diff --git a/scripts/__tests__/ComponentTest/HookTest/UseEffect.test.js b/scripts/__tests__/ComponentTest/HookTest/UseEffect.test.js index 129c2ec3..8f4876f5 100644 --- a/scripts/__tests__/ComponentTest/HookTest/UseEffect.test.js +++ b/scripts/__tests__/ComponentTest/HookTest/UseEffect.test.js @@ -16,7 +16,9 @@ describe('useEffect Hook Test', () => { it('简单使用useEffect', () => { const App = () => { const [num, setNum] = useState(0); + console.log('Render App'); useEffect(() => { + console.log('Effect'); document.getElementById('p').style.display = num === 0 ? 'none' : 'inline'; }); return ( @@ -27,9 +29,11 @@ describe('useEffect Hook Test', () => { ); }; Horizon.render(, container); - expect(document.getElementById('p').style.display).toBe('block'); + expect(document.getElementById('p').style.display).toBe('block'); // <- none 异步 // 点击按钮触发num加1 - container.querySelector('button').click(); + console.log('Click'); + container.querySelector('button').click(); // <- none 异步 + expect(document.getElementById('p').style.display).toBe('none'); container.querySelector('button').click(); expect(container.querySelector('p').style.display).toBe('inline'); diff --git a/scripts/__tests__/DomTest/DomInput.test.js b/scripts/__tests__/DomTest/DomInput.test.js index 7d43b251..2e8ccc23 100755 --- a/scripts/__tests__/DomTest/DomInput.test.js +++ b/scripts/__tests__/DomTest/DomInput.test.js @@ -22,16 +22,6 @@ describe('Dom Input', () => { ).not.toThrow(); }); - it('checked属性受控时无法更改', () => { - Horizon.render( { - LogUtils.log('checkbox click'); - }} />, container); - container.querySelector('input').click(); - // 点击复选框不会改变checked的值 - expect(LogUtils.getAndClear()).toEqual(['checkbox click']); - expect(container.querySelector('input').checked).toBe(true); - }); - it('复选框的value属性值可以改变', () => { Horizon.render( { @@ -96,30 +86,6 @@ describe('Dom Input', () => { ).not.toThrow(); }); - it('value属性受控时无法更改', () => { - const realNode = Horizon.render( { - LogUtils.log('text change'); - }} />, container); - - // 模拟改变text输入框的值 - // 先修改 - Object.getOwnPropertyDescriptor( - HTMLInputElement.prototype, - 'value', - ).set.call(realNode, 'abcd'); - // 再触发事件 - realNode.dispatchEvent( - new Event('input', { - bubbles: true, - cancelable: true, - }), - ); - // 确实发生了input事件 - expect(LogUtils.getAndClear()).toEqual(['text change']); - // value受控,不会改变 - expect(container.querySelector('input').value).toBe('text'); - }); - it('value值会转为字符串', () => { const realNode = Horizon.render(, container); expect(realNode.value).toBe('1'); @@ -249,30 +215,6 @@ describe('Dom Input', () => { expect(document.getElementById('d').checked).toBe(true); }); - it('受控radio的状态', () => { - Horizon.render( - <> - - - , container); - expect(container.querySelector('input').checked).toBe(true); - expect(document.getElementById('b').checked).toBe(false); - Object.getOwnPropertyDescriptor( - HTMLInputElement.prototype, - 'checked', - ).set.call(document.getElementById('b'), true); - // 再触发事件 - document.getElementById('b').dispatchEvent( - new Event('click', { - bubbles: true, - cancelable: true, - }), - ); - // 模拟点击单选框B,两个受控radio的状态不会改变 - expect(container.querySelector('input').checked).toBe(true); - expect(document.getElementById('b').checked).toBe(false); - }); - it('name改变不影响相同name的radio', () => { const inputRef = Horizon.createRef(); const App = () => { diff --git a/scripts/__tests__/DomTest/DomSelect.test.js b/scripts/__tests__/DomTest/DomSelect.test.js index dfd1828e..48932033 100755 --- a/scripts/__tests__/DomTest/DomSelect.test.js +++ b/scripts/__tests__/DomTest/DomSelect.test.js @@ -53,37 +53,6 @@ describe('Dom Select', () => { expect(realNode.value).toBe('React'); }); - it('受控select', () => { - const selectNode = ( - - ); - const realNode = Horizon.render(selectNode, container); - expect(realNode.value).toBe('Vue'); - expect(realNode.options[1].selected).toBe(true); - // 先修改 - Object.getOwnPropertyDescriptor( - HTMLSelectElement.prototype, - 'value', - ).set.call(realNode, 'React'); - // 再触发事件 - container.querySelector('select').dispatchEvent( - new Event('change', { - bubbles: true, - cancelable: true, - }), - ); - // 鼠标改变受控select不生效 - Horizon.render(selectNode, container); - // 'React'项没有被选中 - expect(realNode.options[0].selected).toBe(false); - expect(realNode.options[1].selected).toBe(true); - expect(realNode.value).toBe('Vue'); - }); - it('受控select转为不受控会保存原来select', () => { const selectNode = (