diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e4be6ff..9a01913e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,25 @@ +## 0.0.20 (2022-09-14) +- **core**: #81 fix Memo场景路径错误 + +## 0.0.19 (2022-09-13) +- **core**: fix 弹窗的input可能无法触发onChange事件 + +## 0.0.18 (2022-09-08) +- **core**: fix 键盘事件使用历史记录填充时key为undefined + +## 0.0.17 (2022-09-07) +- **core**: fix 不在树上的节点发起更新导致错误 + +## 0.0.16 (2022-09-07) +- **core**: #56,#65 diff null 不能正确卸载组件 + +## 0.0.15 (2022-09-06) +- **core**: #43 fix portal root 跟 app root重合时重复监听 +- **core**: #38 修复合成事件受普通事件stopPropagation影响无法触发 + +## 0.0.14 (2022-09-04) +- **core**: #44 修复unmount根节点事件未清除 + ## 0.0.13 (2022-08-02) - **horizonX**: 修复redux兼容器bug diff --git a/libs/horizon/index.ts b/libs/horizon/index.ts index f8035375..4c067a3e 100644 --- a/libs/horizon/index.ts +++ b/libs/horizon/index.ts @@ -25,7 +25,7 @@ import { useReducer, useRef, useState, - useDebugValue + useDebugValue, } from './src/renderer/hooks/HookExternal'; import { asyncUpdates } from './src/renderer/TreeBuilder'; import { callRenderQueueImmediate } from './src/renderer/taskExecutor/RenderQueue'; @@ -86,7 +86,7 @@ const Horizon = { useStore, clearStore, reduxAdapter, - watch + watch, }; export const version = __VERSION__; @@ -127,7 +127,7 @@ export { useStore, clearStore, reduxAdapter, - watch + watch, }; export default Horizon; diff --git a/libs/horizon/package.json b/libs/horizon/package.json index b986d476..83d12372 100644 --- a/libs/horizon/package.json +++ b/libs/horizon/package.json @@ -4,7 +4,7 @@ "keywords": [ "horizon" ], - "version": "0.0.13", + "version": "0.0.20", "homepage": "", "bugs": "", "main": "index.js", diff --git a/libs/horizon/src/dom/DOMExternal.ts b/libs/horizon/src/dom/DOMExternal.ts index 5050e3f5..0baa43c2 100644 --- a/libs/horizon/src/dom/DOMExternal.ts +++ b/libs/horizon/src/dom/DOMExternal.ts @@ -1,14 +1,9 @@ -import { - asyncUpdates, getFirstCustomDom, - syncUpdates, startUpdate, - createTreeRootVNode, -} from '../renderer/Renderer'; -import {createPortal} from '../renderer/components/CreatePortal'; -import type {Container} from './DOMOperator'; -import {isElement} from './utils/Common'; -import {listenDelegatedEvents} from '../event/EventBinding'; -import {findDOMByClassInst} from '../renderer/vnode/VNodeUtils'; -import {Callback} from '../renderer/UpdateHandler'; +import { asyncUpdates, getFirstCustomDom, syncUpdates, startUpdate, createTreeRootVNode } from '../renderer/Renderer'; +import { createPortal } from '../renderer/components/CreatePortal'; +import type { Container } from './DOMOperator'; +import { isElement } from './utils/Common'; +import { findDOMByClassInst } from '../renderer/vnode/VNodeUtils'; +import { Callback } from '../renderer/UpdateHandler'; function createRoot(children: any, container: Container, callback?: Callback) { // 清空容器 @@ -39,16 +34,13 @@ function createRoot(children: any, container: Container, callback?: Callback) { return treeRoot; } -function executeRender( - children: any, - container: Container, - callback?: Callback, -) { +function executeRender(children: any, container: Container, callback?: Callback) { let treeRoot = container._treeRoot; if (!treeRoot) { treeRoot = createRoot(children, container, callback); - } else { // container被render过 + } else { + // container被render过 if (typeof callback === 'function') { const cb = callback; callback = function () { @@ -77,11 +69,27 @@ function findDOMNode(domOrEle?: Element): null | Element | Text { return findDOMByClassInst(domOrEle); } +// 情况根节点监听器 +function removeRootEventLister(container: Container) { + const events = (container._treeRoot as any).$EV; + if (events) { + Object.keys(events).forEach(event => { + const listener = events[event]; + + if (listener) { + container.removeEventListener(event, listener); + events[event] = null; + } + }); + } +} + // 卸载入口 -function destroy(container: Container) { - if (container._treeRoot) { +function destroy(container: Container): boolean { + if (container && container._treeRoot) { syncUpdates(() => { executeRender(null, container, () => { + removeRootEventLister(container); container._treeRoot = null; }); }); diff --git a/libs/horizon/src/dom/DOMOperator.ts b/libs/horizon/src/dom/DOMOperator.ts index 07700dd4..4e6b9c2a 100644 --- a/libs/horizon/src/dom/DOMOperator.ts +++ b/libs/horizon/src/dom/DOMOperator.ts @@ -35,7 +35,7 @@ export type Props = Record & { style?: { display?: string }; }; -export type Container = (Element & { _treeRoot?: VNode }) | (Document & { _treeRoot?: VNode }); +export type Container = (Element & { _treeRoot?: VNode | null }) | (Document & { _treeRoot?: VNode | null }); let selectionInfo: null | SelectionData = null; @@ -225,7 +225,3 @@ export function unHideDom(tag: string, dom: Element | Text, props: Props) { dom.textContent = props; } } - -export function prePortal(portal: Element): void { - listenDelegatedEvents(portal); -} diff --git a/libs/horizon/src/dom/DOMPropertiesHandler/DOMPropertiesHandler.ts b/libs/horizon/src/dom/DOMPropertiesHandler/DOMPropertiesHandler.ts index 7864426d..0d9dc259 100644 --- a/libs/horizon/src/dom/DOMPropertiesHandler/DOMPropertiesHandler.ts +++ b/libs/horizon/src/dom/DOMPropertiesHandler/DOMPropertiesHandler.ts @@ -3,7 +3,7 @@ import { updateCommonProp } from './UpdateCommonProp'; import { setStyles } from './StyleHandler'; import { lazyDelegateOnRoot, listenNonDelegatedEvent } from '../../event/EventBinding'; import { isEventProp } from '../validators/ValidateProps'; -import { getCurrentRoot } from '../../renderer/TreeBuilder'; +import { getCurrentRoot } from '../../renderer/RootStack'; // 初始化DOM属性和更新 DOM 属性 export function setDomProps(dom: Element, props: Object, isNativeTag: boolean, isInit: boolean): void { diff --git a/libs/horizon/src/event/EventBinding.ts b/libs/horizon/src/event/EventBinding.ts index 5a38e45b..0623c9fb 100644 --- a/libs/horizon/src/event/EventBinding.ts +++ b/libs/horizon/src/event/EventBinding.ts @@ -35,7 +35,7 @@ function triggerDelegatedEvent( } // 监听委托事件 -function listenToNativeEvent(nativeEvtName: string, delegatedElement: Element, isCapture: boolean): void { +function listenToNativeEvent(nativeEvtName: string, delegatedElement: Element, isCapture: boolean) { let dom: Element | Document = delegatedElement; // document层次可能触发selectionchange事件,为了捕获这类事件,selectionchange事件绑定在document节点上 if (nativeEvtName === 'selectionchange' && !isDocument(delegatedElement)) { @@ -44,22 +44,8 @@ function listenToNativeEvent(nativeEvtName: string, delegatedElement: Element, i const listener = triggerDelegatedEvent.bind(null, nativeEvtName, isCapture, dom); dom.addEventListener(nativeEvtName, listener, isCapture); -} -// 监听所有委托事件 -export function listenDelegatedEvents(dom: Element) { - if (dom[listeningMarker]) { - // 不需要重复注册事件 - return; - } - dom[listeningMarker] = true; - - allDelegatedNativeEvents.forEach((nativeEvtName: string) => { - // 委托冒泡事件 - listenToNativeEvent(nativeEvtName, dom, false); - // 委托捕获事件 - listenToNativeEvent(nativeEvtName, dom, true); - }); + return listener; } // 事件懒委托,当用户定义事件后,再进行委托到根节点 @@ -71,9 +57,17 @@ export function lazyDelegateOnRoot(currentRoot: VNode, eventName: string) { nativeEvents.forEach(nativeEvent => { const nativeFullName = isCapture ? nativeEvent + 'capture' : nativeEvent; - if (!currentRoot.delegatedNativeEvents.has(nativeFullName)) { - listenToNativeEvent(nativeEvent, currentRoot.realNode, isCapture); - currentRoot.delegatedNativeEvents.add(nativeFullName); + + // 事件存储在DOM节点属性,避免多个VNode(root和portal)对应同一个DOM, 造成事件重复监听 + let events = currentRoot.realNode.$EV; + + if (!events) { + events = (currentRoot.realNode as any).$EV = {}; + } + + if (!events[nativeFullName]) { + const listener = listenToNativeEvent(nativeEvent, currentRoot.realNode, isCapture); + events[nativeFullName] = listener; } }); } diff --git a/libs/horizon/src/event/EventWrapper.ts b/libs/horizon/src/event/EventWrapper.ts index 84f5d844..37098a8e 100644 --- a/libs/horizon/src/event/EventWrapper.ts +++ b/libs/horizon/src/event/EventWrapper.ts @@ -25,6 +25,9 @@ export class WrappedEvent { stopPropagation: () => void; preventDefault: () => void; + propagationStopped = false + isPropagationStopped = (): boolean => this.propagationStopped; + // 适配Keyboard键盘事件该函数不能由合成事件调用 getModifierState?: (keyArgs: string) => boolean; // 适配老版本事件api @@ -39,7 +42,11 @@ export class WrappedEvent { } } // stopPropagation和preventDefault 必须通过Event实例调用 - this.stopPropagation = () => nativeEvent.stopPropagation(); + this.stopPropagation = () => { + nativeEvent.stopPropagation(); + this.propagationStopped = true; + }; + this.preventDefault = () => nativeEvent.preventDefault(); // custom事件自定义属性 @@ -51,17 +58,13 @@ export class WrappedEvent { this.type = nativeEvtName; // 兼容IE的event key - const orgKey = (nativeEvent as any).key; + const orgKey = (nativeEvent as any).key ?? ''; this.key = uniqueKeyMap.get(orgKey) || orgKey; } isDefaultPrevented(): boolean { return this.nativeEvent.defaultPrevented; } - - isPropagationStopped(): boolean { - return this.nativeEvent.cancelBubble; - } } // 创建普通自定义事件对象实例,和原生事件对应 diff --git a/libs/horizon/src/external/devtools.ts b/libs/horizon/src/external/devtools.ts index 0cc4d530..1fb1af9b 100644 --- a/libs/horizon/src/external/devtools.ts +++ b/libs/horizon/src/external/devtools.ts @@ -39,7 +39,9 @@ export const helper = { return { name: HookName.RefHook, hIndex, value: (state as Ref).current }; } else if (isEffectHook(state)) { const name = - state.effectConstant == EffectConstant.LayoutEffect ? HookName.LayoutEffectHook : HookName.EffectHook; + state.effectConstant == EffectConstant.LayoutEffect || (EffectConstant.LayoutEffect | EffectConstant.DepsChange) + ? HookName.LayoutEffectHook + : HookName.EffectHook; return { name, hIndex, value: (state as Effect).effect }; } else if (isCallbackHook(state)) { return { name: HookName.CallbackHook, hIndex, value: (state as CallBack).func }; diff --git a/libs/horizon/src/horizonx/proxy/Observer.ts b/libs/horizon/src/horizonx/proxy/Observer.ts index b00e942e..8b33c769 100644 --- a/libs/horizon/src/horizonx/proxy/Observer.ts +++ b/libs/horizon/src/horizonx/proxy/Observer.ts @@ -7,7 +7,6 @@ import { launchUpdateFromVNode } from '../../renderer/TreeBuilder'; import { getProcessingVNode } from '../../renderer/GlobalVar'; import { VNode } from '../../renderer/vnode/VNode'; export interface IObserver { - useProp: (key: string) => void; addListener: (listener: () => void) => void; @@ -21,17 +20,18 @@ export interface IObserver { triggerUpdate: (vNode: any) => void; allChange: () => void; - + clearByVNode: (vNode: any) => void; } export class Observer implements IObserver { - vNodeKeys = new WeakMap(); keyVNodes = new Map(); - listeners:(()=>void)[] = []; + listeners: (() => void)[] = []; + + watchers = {} as { [key: string]: ((key: string, oldValue: any, newValue: any) => void)[] }; watchers={} as {[key:string]:((key:string, oldValue:any, newValue:any)=>void)[]} diff --git a/libs/horizon/src/horizonx/proxy/handlers/ArrayProxyHandler.ts b/libs/horizon/src/horizonx/proxy/handlers/ArrayProxyHandler.ts index dd3fdb0b..391aee8c 100644 --- a/libs/horizon/src/horizonx/proxy/handlers/ArrayProxyHandler.ts +++ b/libs/horizon/src/horizonx/proxy/handlers/ArrayProxyHandler.ts @@ -12,18 +12,18 @@ export function createArrayProxy(rawObj: any[]): any[] { } function get(rawObj: any[], key: string, receiver: any) { - if (key === 'watch'){ + if (key === 'watch') { const observer = getObserver(rawObj); - return (prop:any, handler:(key:string, oldValue:any, newValue:any)=>void)=>{ - if(!observer.watchers[prop]){ - observer.watchers[prop]=[] as ((key:string, oldValue:any, newValue:any)=>void)[]; + return (prop: any, handler: (key: string, oldValue: any, newValue: any) => void) => { + if (!observer.watchers[prop]) { + observer.watchers[prop] = [] as ((key: string, oldValue: any, newValue: any) => void)[]; } observer.watchers[prop].push(handler); - return ()=>{ - observer.watchers[prop]=observer.watchers[prop].filter(cb=>cb!==handler); - } - } + return () => { + observer.watchers[prop] = observer.watchers[prop].filter(cb => cb !== handler); + }; + }; } if (isValidIntegerKey(key) || key === 'length') { @@ -43,7 +43,7 @@ function set(rawObj: any[], key: string, value: any, receiver: any) { const observer = getObserver(rawObj); if (!isSame(newValue, oldValue)) { - if(observer.watchers?.[key]){ + if (observer.watchers?.[key]) { observer.watchers[key].forEach(cb => { cb(key, oldValue, newValue); }); diff --git a/libs/horizon/src/horizonx/proxy/handlers/CollectionProxyHandler.ts b/libs/horizon/src/horizonx/proxy/handlers/CollectionProxyHandler.ts index 15aaa682..99c3a26a 100644 --- a/libs/horizon/src/horizonx/proxy/handlers/CollectionProxyHandler.ts +++ b/libs/horizon/src/horizonx/proxy/handlers/CollectionProxyHandler.ts @@ -34,18 +34,18 @@ function get(rawObj: { size: number }, key: any, receiver: any): any { } else if (Object.prototype.hasOwnProperty.call(handler, key)) { const value = Reflect.get(handler, key, receiver); return value.bind(null, rawObj); - } else if (key === 'watch'){ + } else if (key === 'watch') { const observer = getObserver(rawObj); - return (prop:any, handler:(key:string, oldValue:any, newValue:any)=>void)=>{ - if(!observer.watchers[prop]){ - observer.watchers[prop]=[] as ((key:string, oldValue:any, newValue:any)=>void)[]; + return (prop: any, handler: (key: string, oldValue: any, newValue: any) => void) => { + if (!observer.watchers[prop]) { + observer.watchers[prop] = [] as ((key: string, oldValue: any, newValue: any) => void)[]; } observer.watchers[prop].push(handler); - return ()=>{ - observer.watchers[prop]=observer.watchers[prop].filter(cb=>cb!==handler); - } - } + return () => { + observer.watchers[prop] = observer.watchers[prop].filter(cb => cb !== handler); + }; + }; } return Reflect.get(rawObj, key, receiver); @@ -79,7 +79,7 @@ function set( } if (valChange) { - if(observer.watchers?.[key]){ + if (observer.watchers?.[key]) { observer.watchers[key].forEach(cb => { cb(key, oldValue, newValue); }); diff --git a/libs/horizon/src/horizonx/proxy/watch.ts b/libs/horizon/src/horizonx/proxy/watch.ts index 87f55280..11b65b9e 100644 --- a/libs/horizon/src/horizonx/proxy/watch.ts +++ b/libs/horizon/src/horizonx/proxy/watch.ts @@ -1,8 +1,8 @@ -export function watch(stateVariable:any,listener:(stateVariable:any)=>void){ - listener = listener.bind(null,stateVariable); - stateVariable.addListener(listener); +export function watch(stateVariable: any, listener: (state: any) => void) { + listener = listener.bind(null, stateVariable); + stateVariable.addListener(listener); - return ()=>{ - stateVariable.removeListener(listener); - } -} \ No newline at end of file + return () => { + stateVariable.removeListener(listener); + }; +} diff --git a/libs/horizon/src/renderer/ContextSaver.ts b/libs/horizon/src/renderer/ContextSaver.ts index 2e72ce3c..e0ea8432 100644 --- a/libs/horizon/src/renderer/ContextSaver.ts +++ b/libs/horizon/src/renderer/ContextSaver.ts @@ -7,7 +7,6 @@ import type { VNode, ContextType } from './Types'; import type { Container } from '../dom/DOMOperator'; import { getNSCtx } from '../dom/DOMOperator'; -import { ContextProvider } from './vnode/VNodeTags'; // 保存的是“http://www.w3.org/1999/xhtml”或“http://www.w3.org/2000/svg”, // 用于识别是使用document.createElement()还是使用document.createElementNS()创建DOM @@ -44,32 +43,3 @@ export function resetContext(providerVNode: VNode) { context.value = providerVNode.context; } - -// 在局部更新时,从上到下恢复父节点的context -export function recoverParentContext(vNode: VNode) { - const contextProviders: VNode[] = []; - let parent = vNode.parent; - while (parent !== null) { - if (parent.tag === ContextProvider) { - contextProviders.unshift(parent); - } - parent = parent.parent; - } - contextProviders.forEach(node => { - setContext(node, node.props.value); - }); -} - -// 在局部更新时,从下到上重置父节点的context -export function resetParentContext(vNode: VNode) { - let parent = vNode.parent; - - while (parent !== null) { - if (parent.tag === ContextProvider) { - resetContext(parent); - } - parent = parent.parent; - } -} - - diff --git a/libs/horizon/src/renderer/RootStack.ts b/libs/horizon/src/renderer/RootStack.ts new file mode 100644 index 00000000..8bce59e6 --- /dev/null +++ b/libs/horizon/src/renderer/RootStack.ts @@ -0,0 +1,17 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2022-2022. All rights reserved. + */ + +import { VNode } from './vnode/VNode'; +const currentRootStack: VNode[] = []; +export function getCurrentRoot() { + return currentRootStack[currentRootStack.length - 1]; +} + +export function pushCurrentRoot(root: VNode) { + return currentRootStack.push(root); +} + +export function popCurrentRoot() { + return currentRootStack.pop(); +} diff --git a/libs/horizon/src/renderer/TreeBuilder.ts b/libs/horizon/src/renderer/TreeBuilder.ts index 683c9bc3..d9a10bd7 100644 --- a/libs/horizon/src/renderer/TreeBuilder.ts +++ b/libs/horizon/src/renderer/TreeBuilder.ts @@ -2,7 +2,7 @@ import type { VNode } from './Types'; import { callRenderQueueImmediate, pushRenderCallback } from './taskExecutor/RenderQueue'; import { updateVNode } from './vnode/VNodeCreator'; -import { TreeRoot, DomComponent, DomPortal } from './vnode/VNodeTags'; +import { ContextProvider, DomComponent, DomPortal, TreeRoot } from './vnode/VNodeTags'; import { FlagUtils, InitFlag, Interrupted } from './vnode/VNodeFlags'; import { captureVNode } from './render/BaseComponent'; import { checkLoopingUpdateLimit, submitToRender } from './submit/Submit'; @@ -12,41 +12,44 @@ import componentRenders from './render'; import { BuildCompleted, BuildFatalErrored, - BuildInComplete, getBuildResult, + BuildInComplete, + getBuildResult, getStartVNode, setBuildResult, setProcessingClassVNode, - setStartVNode + setStartVNode, } from './GlobalVar'; import { ByAsync, BySync, - InRender, - InEvent, changeMode, checkMode, copyExecuteMode, + InEvent, + InRender, isExecuting, - setExecuteMode + setExecuteMode, } from './ExecuteMode'; -import { recoverParentContext, resetParentContext, resetNamespaceCtx, setNamespaceCtx } from './ContextSaver'; +import { + resetContext, + resetNamespaceCtx, + setContext, + setNamespaceCtx, +} from './ContextSaver'; import { updateChildShouldUpdate, updateParentsChildShouldUpdate, - updateShouldUpdateOfTree + updateShouldUpdateOfTree, } from './vnode/VNodeShouldUpdate'; import { getPathArr } from './utils/vNodePath'; import { injectUpdater } from '../external/devtools'; +import { popCurrentRoot, pushCurrentRoot } from './RootStack'; // 不可恢复错误 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; @@ -178,7 +181,12 @@ export function calcStartUpdateVNode(treeRoot: VNode) { } if (toUpdateNodes.length === 1) { - return toUpdateNodes[0]; + const toUpdateNode = toUpdateNodes[0]; + if (toUpdateNode.isCleared) { + return treeRoot; + } else { + return toUpdateNodes[0]; + } } // 要计算的节点过多,直接返回根节点 @@ -241,7 +249,7 @@ function buildVNodeTree(treeRoot: VNode) { } // 恢复父节点的context - recoverParentContext(startVNode); + recoverTreeContext(startVNode); } // 重置环境变量,为重新进行深度遍历做准备 @@ -269,7 +277,7 @@ function buildVNodeTree(treeRoot: VNode) { } if (startVNode.tag !== TreeRoot) { // 不是根节点 // 恢复父节点的context - resetParentContext(startVNode); + resetTreeContext(startVNode); } setProcessingClassVNode(null); @@ -277,10 +285,43 @@ function buildVNodeTree(treeRoot: VNode) { setExecuteMode(preMode); } +// 在局部更新时,从上到下恢复父节点的context和PortalStack +function recoverTreeContext(vNode: VNode) { + const contextProviders: VNode[] = []; + let parent = vNode.parent; + while (parent !== null) { + if (parent.tag === ContextProvider) { + contextProviders.unshift(parent); + } + if(parent.tag === DomPortal){ + pushCurrentRoot(parent); + } + parent = parent.parent; + } + contextProviders.forEach(node => { + setContext(node, node.props.value); + }); +} + +// 在局部更新时,从下到上重置父节点的context +function resetTreeContext(vNode: VNode) { + let parent = vNode.parent; + + while (parent !== null) { + if (parent.tag === ContextProvider) { + resetContext(parent); + } + if(parent.tag === DomPortal){ + popCurrentRoot(); + } + parent = parent.parent; + } +} + // 总体任务入口 function renderFromRoot(treeRoot) { runAsyncEffects(); - currentRoot = treeRoot; + pushCurrentRoot(treeRoot); // 1. 构建vNode树 buildVNodeTree(treeRoot); @@ -291,8 +332,7 @@ function renderFromRoot(treeRoot) { // 2. 提交变更 submitToRender(treeRoot); - currentRoot = null; - + popCurrentRoot(); if (window.__HORIZON_DEV_HOOK__) { const hook = window.__HORIZON_DEV_HOOK__; // injector.js 可能在 Horizon 代码之后加载,此时无 __HORIZON_DEV_HOOK__ 全局变量 diff --git a/libs/horizon/src/renderer/diff/nodeDiffComparator.ts b/libs/horizon/src/renderer/diff/nodeDiffComparator.ts index 8e029fb6..686b0692 100644 --- a/libs/horizon/src/renderer/diff/nodeDiffComparator.ts +++ b/libs/horizon/src/renderer/diff/nodeDiffComparator.ts @@ -55,7 +55,7 @@ function deleteVNodes(parentVNode: VNode, startDelVNode: VNode | null, endVNode? } } -function checkCanReuseNode(oldNode: VNode | null, newChild: any): boolean { +function checkCanReuseNode(oldNode: VNode | null, newChild: any, newNodeIdx: number): boolean { if (newChild === null) { return false; } @@ -70,7 +70,13 @@ function checkCanReuseNode(oldNode: VNode | null, newChild: any): boolean { return oldKey === null; } if (newChild.vtype === TYPE_COMMON_ELEMENT || newChild.vtype === TYPE_PORTAL) { - return oldKey === newChild.key; + // key存在时用key判断复用 + if (oldKey !== null || newChild.key !== null) { + return oldKey === newChild.key; + } else { + // 新旧节点的index应该相同才能复用,null会影响位置 + return oldNode?.eIndex === newNodeIdx; + } } } @@ -254,7 +260,7 @@ function diffArrayNodesHandler( nextOldNode = oldNode.next; } - canBeReuse = checkCanReuseNode(oldNode, newChildren[leftIdx]); + canBeReuse = checkCanReuseNode(oldNode, newChildren[leftIdx], leftIdx); // 不能复用,break if (!canBeReuse) { oldNode = oldNode ?? nextOldNode; @@ -295,7 +301,7 @@ function diffArrayNodesHandler( break; } - canBeReuse = checkCanReuseNode(rightOldNode, newChildren[rightIdx - 1]); + canBeReuse = checkCanReuseNode(rightOldNode, newChildren[rightIdx - 1], rightIdx - 1); // 不能复用,break if (!canBeReuse) { break; diff --git a/libs/horizon/src/renderer/render/BaseComponent.ts b/libs/horizon/src/renderer/render/BaseComponent.ts index 4192e100..34ba6b6c 100644 --- a/libs/horizon/src/renderer/render/BaseComponent.ts +++ b/libs/horizon/src/renderer/render/BaseComponent.ts @@ -1,21 +1,16 @@ import type { VNode } from '../Types'; -import { - ContextProvider, - DomComponent, - DomPortal, - TreeRoot, - SuspenseComponent, -} from '../vnode/VNodeTags'; +import { ContextProvider, DomComponent, DomPortal, TreeRoot, SuspenseComponent } from '../vnode/VNodeTags'; import { setContext, setNamespaceCtx } from '../ContextSaver'; import { FlagUtils } from '../vnode/VNodeFlags'; -import {onlyUpdateChildVNodes} from '../vnode/VNodeCreator'; +import { onlyUpdateChildVNodes } from '../vnode/VNodeCreator'; import componentRenders from './index'; -import {setProcessingVNode} from '../GlobalVar'; +import { setProcessingVNode } from '../GlobalVar'; import { clearVNodeObservers } from '../../horizonx/store/StoreHandler'; +import { pushCurrentRoot } from '../RootStack'; -// 复用vNode时,也需对stack进行处理 -function handlerContext(processing: VNode) { +// 复用vNode时,也需对树的上下文值处理,如context,portal, namespaceContext +function setTreeContextValue(processing: VNode) { switch (processing.tag) { case TreeRoot: setNamespaceCtx(processing, processing.realNode); @@ -25,6 +20,7 @@ function handlerContext(processing: VNode) { break; case DomPortal: setNamespaceCtx(processing, processing.realNode); + pushCurrentRoot(processing); break; case ContextProvider: { const newValue = processing.props.value; @@ -40,13 +36,9 @@ export function captureVNode(processing: VNode): VNode | null { if (processing.tag !== SuspenseComponent) { // 该vNode没有变化,不用进入capture,直接复用。 - if ( - !processing.isCreated && - processing.oldProps === processing.props && - !processing.shouldUpdate - ) { + if (!processing.isCreated && processing.oldProps === processing.props && !processing.shouldUpdate) { // 复用还需对stack进行处理 - handlerContext(processing); + setTreeContextValue(processing); return onlyUpdateChildVNodes(processing); } @@ -56,8 +48,8 @@ export function captureVNode(processing: VNode): VNode | null { processing.shouldUpdate = false; setProcessingVNode(processing); - - if(processing.observers) clearVNodeObservers(processing); + + if (processing.observers) clearVNodeObservers(processing); const child = component.captureRender(processing, shouldUpdate); setProcessingVNode(null); diff --git a/libs/horizon/src/renderer/render/DomPortal.ts b/libs/horizon/src/renderer/render/DomPortal.ts index baf4b500..719a5b60 100644 --- a/libs/horizon/src/renderer/render/DomPortal.ts +++ b/libs/horizon/src/renderer/render/DomPortal.ts @@ -1,18 +1,16 @@ import type { VNode } from '../Types'; import { resetNamespaceCtx, setNamespaceCtx } from '../ContextSaver'; import { createChildrenByDiff } from '../diff/nodeDiffComparator'; -import { prePortal } from '../../dom/DOMOperator'; +import { popCurrentRoot, pushCurrentRoot } from '../RootStack'; export function bubbleRender(processing: VNode) { resetNamespaceCtx(processing); - - if (processing.isCreated) { - prePortal(processing.realNode); - } + popCurrentRoot(); } function capturePortalComponent(processing: VNode) { setNamespaceCtx(processing, processing.realNode); + pushCurrentRoot(processing); const newElements = processing.props; if (processing.isCreated) { diff --git a/libs/horizon/src/renderer/vnode/VNode.ts b/libs/horizon/src/renderer/vnode/VNode.ts index 80baedc9..752c99cc 100644 --- a/libs/horizon/src/renderer/vnode/VNode.ts +++ b/libs/horizon/src/renderer/vnode/VNode.ts @@ -39,6 +39,8 @@ export class VNode { ref: RefType | ((handle: any) => void) | null = null; // 包裹一个函数,submit阶段使用,比如将外部useRef生成的对象赋值到ref上 oldProps: any = null; + // 是否已经被从树上移除 + isCleared = false; changeList: any; // DOM的变更列表 effectList: any[] | null; // useEffect 的更新数组 updates: any[] | null; // TreeRoot和ClassComponent使用的更新数组 @@ -78,7 +80,6 @@ export class VNode { // 根节点数据 toUpdateNodes: Set | null; // 保存要更新的节点 delegatedEvents: Set; - delegatedNativeEvents: Set; belongClassVNode: VNode | null = null; // 记录JSXElement所属class vNode,处理ref的时候使用 @@ -100,7 +101,6 @@ export class VNode { this.task = null; this.toUpdateNodes = new Set(); this.delegatedEvents = new Set(); - this.delegatedNativeEvents = new Set(); this.updates = null; this.stateCallbacks = null; this.state = null; @@ -137,6 +137,7 @@ export class VNode { case DomPortal: this.realNode = null; this.context = null; + this.delegatedEvents = new Set(); this.src = null; break; case DomComponent: diff --git a/libs/horizon/src/renderer/vnode/VNodeCreator.ts b/libs/horizon/src/renderer/vnode/VNodeCreator.ts index 79496323..6416a7b7 100644 --- a/libs/horizon/src/renderer/vnode/VNodeCreator.ts +++ b/libs/horizon/src/renderer/vnode/VNodeCreator.ts @@ -230,7 +230,7 @@ export function onlyUpdateChildVNodes(processing: VNode): VNode | null { } }; - putChildrenIntoQueue(processing.child); + putChildrenIntoQueue(processing); while (queue.length) { const vNode = queue.shift()!; diff --git a/libs/horizon/src/renderer/vnode/VNodeUtils.ts b/libs/horizon/src/renderer/vnode/VNodeUtils.ts index f3092976..789b8d2e 100644 --- a/libs/horizon/src/renderer/vnode/VNodeUtils.ts +++ b/libs/horizon/src/renderer/vnode/VNodeUtils.ts @@ -73,6 +73,7 @@ export function travelVNodeTree( // 置空vNode export function clearVNode(vNode: VNode) { + vNode.isCleared = true; vNode.child = null; vNode.next = null; vNode.depContexts = null; diff --git a/scripts/__tests__/ComponentTest/DiffAlgorithm.test.js b/scripts/__tests__/ComponentTest/DiffAlgorithm.test.js new file mode 100644 index 00000000..0f5ab14d --- /dev/null +++ b/scripts/__tests__/ComponentTest/DiffAlgorithm.test.js @@ -0,0 +1,48 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2022-2022. All rights reserved. + */ + +import * as Horizon from '@cloudsop/horizon/index.ts'; + +describe('Diff Algorithm', () => { + it('null should diff correctly', () => { + const fn = jest.fn(); + + class C extends Horizon.Component { + constructor() { + super(); + fn(); + } + + render() { + return 1; + } + } + + let update; + + function App() { + const [current, setCurrent] = Horizon.useState(1); + update = setCurrent; + return ( + <> + {current === 1 ? : null} + {current === 2 ? : null} + {current === 3 ? : null} + + ); + } + + Horizon.render(, container); + expect(fn).toHaveBeenCalledTimes(1); + + update(2); + expect(fn).toHaveBeenCalledTimes(2); + + update(3); + expect(fn).toHaveBeenCalledTimes(3); + + update(1); + expect(fn).toHaveBeenCalledTimes(4); + }); +}); diff --git a/scripts/__tests__/ComponentTest/HookTest/UseEffect.test.js b/scripts/__tests__/ComponentTest/HookTest/UseEffect.test.js index a88cb813..443894a5 100644 --- a/scripts/__tests__/ComponentTest/HookTest/UseEffect.test.js +++ b/scripts/__tests__/ComponentTest/HookTest/UseEffect.test.js @@ -81,7 +81,7 @@ describe('useEffect Hook Test', () => { expect(LogUtils.getAndClear()).toEqual([]); // 在执行新的render前,会执行完上一次render的useEffect,所以LogUtils会加入'NewApp effect'。 Horizon.render([na], container); - expect(LogUtils.getAndClear()).toEqual(['NewApp effect']); + expect(LogUtils.getAndClear()).toEqual(['NewApp effect', 'NewApp']); expect(container.textContent).toBe('NewApp'); expect(LogUtils.getAndClear()).toEqual([]); }); diff --git a/scripts/__tests__/ComponentTest/Memo.test.js b/scripts/__tests__/ComponentTest/Memo.test.js new file mode 100644 index 00000000..b1a6fa9f --- /dev/null +++ b/scripts/__tests__/ComponentTest/Memo.test.js @@ -0,0 +1,41 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2022-2022. All rights reserved. + */ + +import * as Horizon from '@cloudsop/horizon/index.ts'; + +describe('Memo Test', () => { + it('Memo should not make the path wrong', function () { + let updateApp; + + function Child() { + const [_, update] = Horizon.useState({}); + updateApp = () => update({}); + return
; + } + const MemoChild = Horizon.memo(Child); + + function App() { + return ( +
+ +
+ ); + } + const MemoApp = Horizon.memo(App); + Horizon.render( +
+ +
, + container + ); + Horizon.render( +
+ + +
, + container + ); + expect(() => updateApp()).not.toThrow(); + }); +}); diff --git a/scripts/__tests__/ComponentTest/PortalComponent.test.js b/scripts/__tests__/ComponentTest/PortalComponent.test.js index 77811c2d..2dcadc38 100755 --- a/scripts/__tests__/ComponentTest/PortalComponent.test.js +++ b/scripts/__tests__/ComponentTest/PortalComponent.test.js @@ -1,9 +1,10 @@ import * as Horizon from '@cloudsop/horizon/index.ts'; import { getLogUtils } from '../jest/testUtils'; +import dispatchChangeEvent from '../utils/dispatchChangeEvent'; describe('PortalComponent Test', () => { const LogUtils = getLogUtils(); - + it('将子节点渲染到存在于父组件以外的 DOM 节点', () => { const portalRoot = document.createElement('div'); @@ -14,12 +15,10 @@ describe('PortalComponent Test', () => { } render() { - return Horizon.createPortal( - this.props.child, - this.element, - ); + return Horizon.createPortal(this.props.child, this.element); } } + Horizon.render(PortalApp} />, container); expect(container.textContent).toBe(''); //
PortalApp
被渲染到了portalRoot而非container @@ -43,17 +42,12 @@ describe('PortalComponent Test', () => { render() { return [ - Horizon.createPortal( - this.props.child, - this.element, - ), - Horizon.createPortal( - this.props.child, - this.newElement, - ) + Horizon.createPortal(this.props.child, this.element), + Horizon.createPortal(this.props.child, this.newElement), ]; } } + Horizon.render(PortalApp} />, container); expect(container.textContent).toBe(''); //
PortalApp
被渲染到了portalRoot而非container @@ -82,21 +76,16 @@ describe('PortalComponent Test', () => { render() { return [
PortalApp1st
, - Horizon.createPortal([ -
PortalApp4
, - Horizon.createPortal( - this.props.child, - this.element3rd, - ), - ], this.element), -
PortalApp2nd
, Horizon.createPortal( - this.props.child, - this.newElement, - ) + [
PortalApp4
, Horizon.createPortal(this.props.child, this.element3rd)], + this.element + ), +
PortalApp2nd
, + Horizon.createPortal(this.props.child, this.newElement), ]; } } + Horizon.render(PortalApp} />, container); expect(container.textContent).toBe('PortalApp1stPortalApp2nd'); //
PortalApp4
会挂载在this.element上 @@ -120,25 +109,23 @@ describe('PortalComponent Test', () => { } render() { - return Horizon.createPortal( - this.props.child, - this.element, - ); + return Horizon.createPortal(this.props.child, this.element); } } - Horizon.render(PortalApp} />, container); + + Horizon.render(PortalApp} />, container); expect(container.textContent).toBe(''); expect(portalRoot.textContent).toBe('PortalApp'); - Horizon.render(AppPortal} />, container); + Horizon.render(AppPortal} />, container); expect(container.textContent).toBe(''); expect(portalRoot.textContent).toBe('AppPortal'); - Horizon.render(, container); + Horizon.render(, container); expect(container.textContent).toBe(''); expect(portalRoot.textContent).toBe('portal'); - Horizon.render(, container); + Horizon.render(, container); expect(container.textContent).toBe(''); expect(portalRoot.textContent).toBe(''); @@ -158,10 +145,7 @@ describe('PortalComponent Test', () => { } render() { - return Horizon.createPortal( - this.props.child, - this.element, - ); + return Horizon.createPortal(this.props.child, this.element); } } @@ -173,7 +157,6 @@ describe('PortalComponent Test', () => { ); }; - const App = () => { const handleClick = () => { LogUtils.log('bubble click event'); @@ -185,9 +168,7 @@ describe('PortalComponent Test', () => { return (
- }> - - + }>
); }; @@ -199,7 +180,95 @@ describe('PortalComponent Test', () => { expect(LogUtils.getAndClear()).toEqual([ // 从外到内先捕获再冒泡 'capture click event', - 'bubble click event' + 'bubble click event', ]); }); -}); \ No newline at end of file + + it('Create portal at app root should not add event listener multiple times', () => { + const btnRef = Horizon.createRef(); + + class PortalApp extends Horizon.Component { + constructor(props) { + super(props); + } + + render() { + return Horizon.createPortal(this.props.child, container); + } + } + + const onClick = jest.fn(); + + class App extends Horizon.Component { + constructor(props) { + super(props); + } + + render() { + return ( +
+ + +
+ ); + } + } + + Horizon.render(, container); + btnRef.current.click(); + expect(onClick).toHaveBeenCalledTimes(1); + }); + + it('#76 Portal onChange should activate', () => { + class Dialog extends Horizon.Component { + node; + + constructor(props) { + super(props); + this.node = window.document.createElement('div'); + window.document.body.appendChild(this.node); + } + + render() { + return Horizon.createPortal(this.props.children, this.node); + } + } + + let showPortalInput; + const fn = jest.fn(); + const inputRef = Horizon.createRef(); + + function App() { + const Input = () => { + const [show, setShow] = Horizon.useState(false); + showPortalInput = setShow; + + Horizon.useEffect(() => { + setTimeout(() => { + setShow(true); + }, 0); + }, []); + + if (!show) { + return null; + } + + return ; + }; + + return ( +
+ + + +
+ ); + } + + Horizon.render(, container); + showPortalInput(true); + jest.advanceTimersToNextTimer(); + dispatchChangeEvent(inputRef.current, 'test'); + expect(fn).toHaveBeenCalledTimes(1); + }); +}); diff --git a/scripts/__tests__/HorizonXText/StoreFunctionality/watch.test.tsx b/scripts/__tests__/HorizonXText/StoreFunctionality/watch.test.tsx index 806c6029..d423f3bf 100644 --- a/scripts/__tests__/HorizonXText/StoreFunctionality/watch.test.tsx +++ b/scripts/__tests__/HorizonXText/StoreFunctionality/watch.test.tsx @@ -1,132 +1,130 @@ -import { createStore } from "@cloudsop/horizon/src/horizonx/store/StoreHandler"; -import { watch } from "@cloudsop/horizon/src/horizonx/proxy/watch"; +import { createStore } from '@cloudsop/horizon/src/horizonx/store/StoreHandler'; +import { watch } from '@cloudsop/horizon/src/horizonx/proxy/watch'; -describe("watch",()=>{ - it('shouhld watch promitive state variable', async()=>{ - const useStore = createStore({ - state:{ - variable:'x' - }, - actions:{ - change:(state)=>state.variable = "a" - } - }); - - const store = useStore(); - let counter = 0; - - watch(store.$s,(state)=>{ - counter++; - expect(state.variable).toBe('a'); - }) - - store.change(); - - expect(counter).toBe(1); - }); - it('shouhld watch object variable', async()=>{ - const useStore = createStore({ - state:{ - variable:'x' - }, - actions:{ - change:(state)=>state.variable = "a" - } - }); - - const store = useStore(); - let counter = 0; - - store.$s.watch('variable',()=>{ - counter++; - }) - - store.change(); - - expect(counter).toBe(1); +describe('watch', () => { + it('shouhld watch promitive state variable', async () => { + const useStore = createStore({ + state: { + variable: 'x', + }, + actions: { + change: state => (state.variable = 'a'), + }, }); - it('shouhld watch array item', async()=>{ - const useStore = createStore({ - state:{ - arr:['x'] - }, - actions:{ - change:(state)=>state.arr[0]='a' - } - }); + const store = useStore(); + let counter = 0; - const store = useStore(); - let counter = 0; - - store.arr.watch('0',()=>{ - counter++; - }) - - store.change(); - - expect(counter).toBe(1); + watch(store.$s, state => { + counter++; + expect(state.variable).toBe('a'); }); - it('shouhld watch collection item', async()=>{ - const useStore = createStore({ - state:{ - collection:new Map([ - ['a', 'a'], - ]) - }, - actions:{ - change:(state)=>state.collection.set('a','x') - } - }); + store.change(); - const store = useStore(); - let counter = 0; - - store.collection.watch('a',()=>{ - counter++; - }) - - store.change(); - - expect(counter).toBe(1); + expect(counter).toBe(1); + }); + it('shouhld watch object variable', async () => { + const useStore = createStore({ + state: { + variable: 'x', + }, + actions: { + change: state => (state.variable = 'a'), + }, }); - it('should watch multiple variables independedntly', async()=>{ - const useStore = createStore({ - state:{ - bool1:true, - bool2:false - }, - actions:{ - toggle1:state=>state.bool1=!state.bool1, - toggle2:state=>state.bool2=!state.bool2 - } - }); + const store = useStore(); + let counter = 0; - let counter1=0; - let counterAll=0; - const store = useStore(); + store.$s.watch('variable', () => { + counter++; + }); - watch(store.$s,()=>{ - counterAll++; - }) + store.change(); - store.$s.watch('bool1',()=>{ - counter1++; - }); + expect(counter).toBe(1); + }); - store.toggle1(); - store.toggle1(); + it('shouhld watch array item', async () => { + const useStore = createStore({ + state: { + arr: ['x'], + }, + actions: { + change: state => (state.arr[0] = 'a'), + }, + }); - store.toggle2(); + const store = useStore(); + let counter = 0; - store.toggle1(); + store.arr.watch('0', () => { + counter++; + }); - store.toggle2(); - store.toggle2(); + store.change(); - expect(counter1).toBe(3); - expect(counterAll).toBe(6); - }) -}) \ No newline at end of file + expect(counter).toBe(1); + }); + + it('shouhld watch collection item', async () => { + const useStore = createStore({ + state: { + collection: new Map([['a', 'a']]), + }, + actions: { + change: state => state.collection.set('a', 'x'), + }, + }); + + const store = useStore(); + let counter = 0; + + store.collection.watch('a', () => { + counter++; + }); + + store.change(); + + expect(counter).toBe(1); + }); + + it('should watch multiple variables independedntly', async () => { + const useStore = createStore({ + state: { + bool1: true, + bool2: false, + }, + actions: { + toggle1: state => (state.bool1 = !state.bool1), + toggle2: state => (state.bool2 = !state.bool2), + }, + }); + + let counter1 = 0; + let counterAll = 0; + const store = useStore(); + + watch(store.$s, () => { + counterAll++; + }); + + store.$s.watch('bool1', () => { + counter1++; + }); + + store.toggle1(); + store.toggle1(); + + store.toggle2(); + + store.toggle1(); + + store.toggle2(); + store.toggle2(); + + expect(counter1).toBe(3); + expect(counterAll).toBe(6); + }); +}); diff --git a/scripts/__tests__/HorizonXText/edgeCases/multipleStores.test.tsx b/scripts/__tests__/HorizonXText/edgeCases/multipleStores.test.tsx index 63fb74e5..9c9f847c 100644 --- a/scripts/__tests__/HorizonXText/edgeCases/multipleStores.test.tsx +++ b/scripts/__tests__/HorizonXText/edgeCases/multipleStores.test.tsx @@ -6,20 +6,20 @@ import { describe, beforeEach, afterEach, it, expect } from '@jest/globals'; const { unmountComponentAtNode } = Horizon; const useStore1 = createStore({ - state:{ counter:1 }, - actions:{ - add:(state)=>state.counter++, - reset: (state)=>state.counter=1 - } -}) + state: { counter: 1 }, + actions: { + add: state => state.counter++, + reset: state => (state.counter = 1), + }, +}); const useStore2 = createStore({ - state:{ counter2:1 }, - actions:{ - add2:(state)=>state.counter2++, - reset: (state)=>state.counter2=1 - } -}) + state: { counter2: 1 }, + actions: { + add2: state => state.counter2++, + reset: state => (state.counter2 = 1), + }, +}); describe('Using multiple stores', () => { let container: HTMLElement | null = null; @@ -42,10 +42,10 @@ describe('Using multiple stores', () => { }); it('Should use multiple stores in class component', () => { - class App extends Horizon.Component{ - render(){ - const {counter,add} = useStore1(); - const {counter2, add2} = useStore2(); + class App extends Horizon.Component { + render() { + const { counter, add } = useStore1(); + const { counter2, add2 } = useStore2(); return (
@@ -65,9 +65,11 @@ describe('Using multiple stores', () => { > add -

{counter} {counter2}

+

+ {counter} {counter2} +

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

{counter} {counter2}

+

+ {counter} {counter2} +

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

{counter} {counter2}

+

+ {counter} {counter2} +

); } @@ -186,14 +186,12 @@ describe('Using multiple stores', () => { expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('1 1'); Horizon.act(() => { triggerClickEvent(container, BUTTON_ID); - }); expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('2 1'); Horizon.act(() => { triggerClickEvent(container, BUTTON_ID2); - }); expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('2 2'); }); -}); \ No newline at end of file +}); diff --git a/scripts/__tests__/utils/dispatchChangeEvent.js b/scripts/__tests__/utils/dispatchChangeEvent.js new file mode 100644 index 00000000..1b8bbd04 --- /dev/null +++ b/scripts/__tests__/utils/dispatchChangeEvent.js @@ -0,0 +1,9 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2022-2022. All rights reserved. + */ +export default function dispatchChangeEvent(inputEle, value) { + const nativeInputSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set; + nativeInputSetter.call(inputEle, value); + + inputEle.dispatchEvent(new Event('input', { bubbles: true})); +}