Match-id-5d3fe224de6eaeeb3682e388a086c506c36fdcb4

This commit is contained in:
* 2022-09-19 17:30:15 +08:00
commit 8d9b6310f1
29 changed files with 578 additions and 365 deletions

View File

@ -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) ## 0.0.13 (2022-08-02)
- **horizonX**: 修复redux兼容器bug - **horizonX**: 修复redux兼容器bug

View File

@ -25,7 +25,7 @@ import {
useReducer, useReducer,
useRef, useRef,
useState, useState,
useDebugValue useDebugValue,
} from './src/renderer/hooks/HookExternal'; } from './src/renderer/hooks/HookExternal';
import { asyncUpdates } from './src/renderer/TreeBuilder'; import { asyncUpdates } from './src/renderer/TreeBuilder';
import { callRenderQueueImmediate } from './src/renderer/taskExecutor/RenderQueue'; import { callRenderQueueImmediate } from './src/renderer/taskExecutor/RenderQueue';
@ -86,7 +86,7 @@ const Horizon = {
useStore, useStore,
clearStore, clearStore,
reduxAdapter, reduxAdapter,
watch watch,
}; };
export const version = __VERSION__; export const version = __VERSION__;
@ -127,7 +127,7 @@ export {
useStore, useStore,
clearStore, clearStore,
reduxAdapter, reduxAdapter,
watch watch,
}; };
export default Horizon; export default Horizon;

View File

@ -4,7 +4,7 @@
"keywords": [ "keywords": [
"horizon" "horizon"
], ],
"version": "0.0.13", "version": "0.0.20",
"homepage": "", "homepage": "",
"bugs": "", "bugs": "",
"main": "index.js", "main": "index.js",

View File

@ -1,12 +1,7 @@
import { import { asyncUpdates, getFirstCustomDom, syncUpdates, startUpdate, createTreeRootVNode } from '../renderer/Renderer';
asyncUpdates, getFirstCustomDom,
syncUpdates, startUpdate,
createTreeRootVNode,
} from '../renderer/Renderer';
import { createPortal } from '../renderer/components/CreatePortal'; import { createPortal } from '../renderer/components/CreatePortal';
import type { Container } from './DOMOperator'; import type { Container } from './DOMOperator';
import { isElement } from './utils/Common'; import { isElement } from './utils/Common';
import {listenDelegatedEvents} from '../event/EventBinding';
import { findDOMByClassInst } from '../renderer/vnode/VNodeUtils'; import { findDOMByClassInst } from '../renderer/vnode/VNodeUtils';
import { Callback } from '../renderer/UpdateHandler'; import { Callback } from '../renderer/UpdateHandler';
@ -39,16 +34,13 @@ function createRoot(children: any, container: Container, callback?: Callback) {
return treeRoot; return treeRoot;
} }
function executeRender( function executeRender(children: any, container: Container, callback?: Callback) {
children: any,
container: Container,
callback?: Callback,
) {
let treeRoot = container._treeRoot; let treeRoot = container._treeRoot;
if (!treeRoot) { if (!treeRoot) {
treeRoot = createRoot(children, container, callback); treeRoot = createRoot(children, container, callback);
} else { // container被render过 } else {
// container被render过
if (typeof callback === 'function') { if (typeof callback === 'function') {
const cb = callback; const cb = callback;
callback = function () { callback = function () {
@ -77,11 +69,27 @@ function findDOMNode(domOrEle?: Element): null | Element | Text {
return findDOMByClassInst(domOrEle); 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) { function destroy(container: Container): boolean {
if (container._treeRoot) { if (container && container._treeRoot) {
syncUpdates(() => { syncUpdates(() => {
executeRender(null, container, () => { executeRender(null, container, () => {
removeRootEventLister(container);
container._treeRoot = null; container._treeRoot = null;
}); });
}); });

View File

@ -35,7 +35,7 @@ export type Props = Record<string, any> & {
style?: { display?: string }; 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; let selectionInfo: null | SelectionData = null;
@ -225,7 +225,3 @@ export function unHideDom(tag: string, dom: Element | Text, props: Props) {
dom.textContent = props; dom.textContent = props;
} }
} }
export function prePortal(portal: Element): void {
listenDelegatedEvents(portal);
}

View File

@ -3,7 +3,7 @@ import { updateCommonProp } from './UpdateCommonProp';
import { setStyles } from './StyleHandler'; import { setStyles } from './StyleHandler';
import { lazyDelegateOnRoot, listenNonDelegatedEvent } from '../../event/EventBinding'; import { lazyDelegateOnRoot, listenNonDelegatedEvent } from '../../event/EventBinding';
import { isEventProp } from '../validators/ValidateProps'; import { isEventProp } from '../validators/ValidateProps';
import { getCurrentRoot } from '../../renderer/TreeBuilder'; import { getCurrentRoot } from '../../renderer/RootStack';
// 初始化DOM属性和更新 DOM 属性 // 初始化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 {

View File

@ -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; let dom: Element | Document = delegatedElement;
// document层次可能触发selectionchange事件为了捕获这类事件selectionchange事件绑定在document节点上 // document层次可能触发selectionchange事件为了捕获这类事件selectionchange事件绑定在document节点上
if (nativeEvtName === 'selectionchange' && !isDocument(delegatedElement)) { if (nativeEvtName === 'selectionchange' && !isDocument(delegatedElement)) {
@ -44,22 +44,8 @@ function listenToNativeEvent(nativeEvtName: string, delegatedElement: Element, i
const listener = triggerDelegatedEvent.bind(null, nativeEvtName, isCapture, dom); const listener = triggerDelegatedEvent.bind(null, nativeEvtName, isCapture, dom);
dom.addEventListener(nativeEvtName, listener, isCapture); dom.addEventListener(nativeEvtName, listener, isCapture);
}
// 监听所有委托事件 return listener;
export function listenDelegatedEvents(dom: Element) {
if (dom[listeningMarker]) {
// 不需要重复注册事件
return;
}
dom[listeningMarker] = true;
allDelegatedNativeEvents.forEach((nativeEvtName: string) => {
// 委托冒泡事件
listenToNativeEvent(nativeEvtName, dom, false);
// 委托捕获事件
listenToNativeEvent(nativeEvtName, dom, true);
});
} }
// 事件懒委托,当用户定义事件后,再进行委托到根节点 // 事件懒委托,当用户定义事件后,再进行委托到根节点
@ -71,9 +57,17 @@ export function lazyDelegateOnRoot(currentRoot: VNode, eventName: string) {
nativeEvents.forEach(nativeEvent => { nativeEvents.forEach(nativeEvent => {
const nativeFullName = isCapture ? nativeEvent + 'capture' : nativeEvent; const nativeFullName = isCapture ? nativeEvent + 'capture' : nativeEvent;
if (!currentRoot.delegatedNativeEvents.has(nativeFullName)) {
listenToNativeEvent(nativeEvent, currentRoot.realNode, isCapture); // 事件存储在DOM节点属性避免多个VNode(root和portal)对应同一个DOM, 造成事件重复监听
currentRoot.delegatedNativeEvents.add(nativeFullName); 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;
} }
}); });
} }

View File

@ -25,6 +25,9 @@ export class WrappedEvent {
stopPropagation: () => void; stopPropagation: () => void;
preventDefault: () => void; preventDefault: () => void;
propagationStopped = false
isPropagationStopped = (): boolean => this.propagationStopped;
// 适配Keyboard键盘事件该函数不能由合成事件调用 // 适配Keyboard键盘事件该函数不能由合成事件调用
getModifierState?: (keyArgs: string) => boolean; getModifierState?: (keyArgs: string) => boolean;
// 适配老版本事件api // 适配老版本事件api
@ -39,7 +42,11 @@ export class WrappedEvent {
} }
} }
// stopPropagation和preventDefault 必须通过Event实例调用 // stopPropagation和preventDefault 必须通过Event实例调用
this.stopPropagation = () => nativeEvent.stopPropagation(); this.stopPropagation = () => {
nativeEvent.stopPropagation();
this.propagationStopped = true;
};
this.preventDefault = () => nativeEvent.preventDefault(); this.preventDefault = () => nativeEvent.preventDefault();
// custom事件自定义属性 // custom事件自定义属性
@ -51,17 +58,13 @@ export class WrappedEvent {
this.type = nativeEvtName; this.type = nativeEvtName;
// 兼容IE的event key // 兼容IE的event key
const orgKey = (nativeEvent as any).key; const orgKey = (nativeEvent as any).key ?? '';
this.key = uniqueKeyMap.get(orgKey) || orgKey; this.key = uniqueKeyMap.get(orgKey) || orgKey;
} }
isDefaultPrevented(): boolean { isDefaultPrevented(): boolean {
return this.nativeEvent.defaultPrevented; return this.nativeEvent.defaultPrevented;
} }
isPropagationStopped(): boolean {
return this.nativeEvent.cancelBubble;
}
} }
// 创建普通自定义事件对象实例,和原生事件对应 // 创建普通自定义事件对象实例,和原生事件对应

View File

@ -39,7 +39,9 @@ export const helper = {
return { name: HookName.RefHook, hIndex, value: (state as Ref<any>).current }; return { name: HookName.RefHook, hIndex, value: (state as Ref<any>).current };
} else if (isEffectHook(state)) { } else if (isEffectHook(state)) {
const name = 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 }; return { name, hIndex, value: (state as Effect).effect };
} else if (isCallbackHook(state)) { } else if (isCallbackHook(state)) {
return { name: HookName.CallbackHook, hIndex, value: (state as CallBack<any>).func }; return { name: HookName.CallbackHook, hIndex, value: (state as CallBack<any>).func };

View File

@ -7,7 +7,6 @@ import { launchUpdateFromVNode } from '../../renderer/TreeBuilder';
import { getProcessingVNode } from '../../renderer/GlobalVar'; import { getProcessingVNode } from '../../renderer/GlobalVar';
import { VNode } from '../../renderer/vnode/VNode'; import { VNode } from '../../renderer/vnode/VNode';
export interface IObserver { export interface IObserver {
useProp: (key: string) => void; useProp: (key: string) => void;
addListener: (listener: () => void) => void; addListener: (listener: () => void) => void;
@ -26,13 +25,14 @@ export interface IObserver {
} }
export class Observer implements IObserver { export class Observer implements IObserver {
vNodeKeys = new WeakMap(); vNodeKeys = new WeakMap();
keyVNodes = new Map(); 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)[]} watchers={} as {[key:string]:((key:string, oldValue:any, newValue:any)=>void)[]}
useProp(key: string | symbol): void { useProp(key: string | symbol): void {

View File

@ -22,8 +22,8 @@ function get(rawObj: any[], key: string, receiver: any) {
observer.watchers[prop].push(handler); observer.watchers[prop].push(handler);
return () => { return () => {
observer.watchers[prop] = observer.watchers[prop].filter(cb => cb !== handler); observer.watchers[prop] = observer.watchers[prop].filter(cb => cb !== handler);
} };
} };
} }
if (isValidIntegerKey(key) || key === 'length') { if (isValidIntegerKey(key) || key === 'length') {

View File

@ -44,8 +44,8 @@ function get(rawObj: { size: number }, key: any, receiver: any): any {
observer.watchers[prop].push(handler); observer.watchers[prop].push(handler);
return () => { return () => {
observer.watchers[prop] = observer.watchers[prop].filter(cb => cb !== handler); observer.watchers[prop] = observer.watchers[prop].filter(cb => cb !== handler);
} };
} };
} }
return Reflect.get(rawObj, key, receiver); return Reflect.get(rawObj, key, receiver);

View File

@ -1,8 +1,8 @@
export function watch(stateVariable:any,listener:(stateVariable:any)=>void){ export function watch(stateVariable: any, listener: (state: any) => void) {
listener = listener.bind(null, stateVariable); listener = listener.bind(null, stateVariable);
stateVariable.addListener(listener); stateVariable.addListener(listener);
return () => { return () => {
stateVariable.removeListener(listener); stateVariable.removeListener(listener);
} };
} }

View File

@ -7,7 +7,6 @@ import type { VNode, ContextType } from './Types';
import type { Container } from '../dom/DOMOperator'; import type { Container } from '../dom/DOMOperator';
import { getNSCtx } 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” // 保存的是“http://www.w3.org/1999/xhtml”或“http://www.w3.org/2000/svg”
// 用于识别是使用document.createElement()还是使用document.createElementNS()创建DOM // 用于识别是使用document.createElement()还是使用document.createElementNS()创建DOM
@ -44,32 +43,3 @@ export function resetContext(providerVNode: VNode) {
context.value = providerVNode.context; 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;
}
}

View File

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

View File

@ -2,7 +2,7 @@ import type { VNode } from './Types';
import { callRenderQueueImmediate, pushRenderCallback } from './taskExecutor/RenderQueue'; import { callRenderQueueImmediate, pushRenderCallback } from './taskExecutor/RenderQueue';
import { updateVNode } from './vnode/VNodeCreator'; 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 { FlagUtils, InitFlag, Interrupted } from './vnode/VNodeFlags';
import { captureVNode } from './render/BaseComponent'; import { captureVNode } from './render/BaseComponent';
import { checkLoopingUpdateLimit, submitToRender } from './submit/Submit'; import { checkLoopingUpdateLimit, submitToRender } from './submit/Submit';
@ -12,41 +12,44 @@ import componentRenders from './render';
import { import {
BuildCompleted, BuildCompleted,
BuildFatalErrored, BuildFatalErrored,
BuildInComplete, getBuildResult, BuildInComplete,
getBuildResult,
getStartVNode, getStartVNode,
setBuildResult, setBuildResult,
setProcessingClassVNode, setProcessingClassVNode,
setStartVNode setStartVNode,
} from './GlobalVar'; } from './GlobalVar';
import { import {
ByAsync, ByAsync,
BySync, BySync,
InRender,
InEvent,
changeMode, changeMode,
checkMode, checkMode,
copyExecuteMode, copyExecuteMode,
InEvent,
InRender,
isExecuting, isExecuting,
setExecuteMode setExecuteMode,
} from './ExecuteMode'; } from './ExecuteMode';
import { recoverParentContext, resetParentContext, resetNamespaceCtx, setNamespaceCtx } from './ContextSaver'; import {
resetContext,
resetNamespaceCtx,
setContext,
setNamespaceCtx,
} from './ContextSaver';
import { import {
updateChildShouldUpdate, updateChildShouldUpdate,
updateParentsChildShouldUpdate, updateParentsChildShouldUpdate,
updateShouldUpdateOfTree updateShouldUpdateOfTree,
} from './vnode/VNodeShouldUpdate'; } from './vnode/VNodeShouldUpdate';
import { getPathArr } from './utils/vNodePath'; import { getPathArr } from './utils/vNodePath';
import { injectUpdater } from '../external/devtools'; import { injectUpdater } from '../external/devtools';
import { popCurrentRoot, pushCurrentRoot } from './RootStack';
// 不可恢复错误 // 不可恢复错误
let unrecoverableErrorDuringBuild: any = null; let unrecoverableErrorDuringBuild: any = null;
// 当前运行的vNode节点 // 当前运行的vNode节点
let processing: VNode | null = null; let processing: VNode | null = null;
let currentRoot: VNode | null = null;
export function getCurrentRoot() {
return currentRoot;
}
export function setProcessing(vNode: VNode | null) { export function setProcessing(vNode: VNode | null) {
processing = vNode; processing = vNode;
@ -178,8 +181,13 @@ export function calcStartUpdateVNode(treeRoot: VNode) {
} }
if (toUpdateNodes.length === 1) { if (toUpdateNodes.length === 1) {
const toUpdateNode = toUpdateNodes[0];
if (toUpdateNode.isCleared) {
return treeRoot;
} else {
return toUpdateNodes[0]; return toUpdateNodes[0];
} }
}
// 要计算的节点过多,直接返回根节点 // 要计算的节点过多,直接返回根节点
if (toUpdateNodes.length > 100) { if (toUpdateNodes.length > 100) {
@ -241,7 +249,7 @@ function buildVNodeTree(treeRoot: VNode) {
} }
// 恢复父节点的context // 恢复父节点的context
recoverParentContext(startVNode); recoverTreeContext(startVNode);
} }
// 重置环境变量,为重新进行深度遍历做准备 // 重置环境变量,为重新进行深度遍历做准备
@ -269,7 +277,7 @@ function buildVNodeTree(treeRoot: VNode) {
} }
if (startVNode.tag !== TreeRoot) { // 不是根节点 if (startVNode.tag !== TreeRoot) { // 不是根节点
// 恢复父节点的context // 恢复父节点的context
resetParentContext(startVNode); resetTreeContext(startVNode);
} }
setProcessingClassVNode(null); setProcessingClassVNode(null);
@ -277,10 +285,43 @@ function buildVNodeTree(treeRoot: VNode) {
setExecuteMode(preMode); 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) { function renderFromRoot(treeRoot) {
runAsyncEffects(); runAsyncEffects();
currentRoot = treeRoot; pushCurrentRoot(treeRoot);
// 1. 构建vNode树 // 1. 构建vNode树
buildVNodeTree(treeRoot); buildVNodeTree(treeRoot);
@ -291,8 +332,7 @@ function renderFromRoot(treeRoot) {
// 2. 提交变更 // 2. 提交变更
submitToRender(treeRoot); submitToRender(treeRoot);
currentRoot = null; popCurrentRoot();
if (window.__HORIZON_DEV_HOOK__) { if (window.__HORIZON_DEV_HOOK__) {
const hook = window.__HORIZON_DEV_HOOK__; const hook = window.__HORIZON_DEV_HOOK__;
// injector.js 可能在 Horizon 代码之后加载,此时无 __HORIZON_DEV_HOOK__ 全局变量 // injector.js 可能在 Horizon 代码之后加载,此时无 __HORIZON_DEV_HOOK__ 全局变量

View File

@ -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) { if (newChild === null) {
return false; return false;
} }
@ -70,7 +70,13 @@ function checkCanReuseNode(oldNode: VNode | null, newChild: any): boolean {
return oldKey === null; return oldKey === null;
} }
if (newChild.vtype === TYPE_COMMON_ELEMENT || newChild.vtype === TYPE_PORTAL) { if (newChild.vtype === TYPE_COMMON_ELEMENT || newChild.vtype === TYPE_PORTAL) {
// key存在时用key判断复用
if (oldKey !== null || newChild.key !== null) {
return oldKey === newChild.key; return oldKey === newChild.key;
} else {
// 新旧节点的index应该相同才能复用null会影响位置
return oldNode?.eIndex === newNodeIdx;
}
} }
} }
@ -254,7 +260,7 @@ function diffArrayNodesHandler(
nextOldNode = oldNode.next; nextOldNode = oldNode.next;
} }
canBeReuse = checkCanReuseNode(oldNode, newChildren[leftIdx]); canBeReuse = checkCanReuseNode(oldNode, newChildren[leftIdx], leftIdx);
// 不能复用break // 不能复用break
if (!canBeReuse) { if (!canBeReuse) {
oldNode = oldNode ?? nextOldNode; oldNode = oldNode ?? nextOldNode;
@ -295,7 +301,7 @@ function diffArrayNodesHandler(
break; break;
} }
canBeReuse = checkCanReuseNode(rightOldNode, newChildren[rightIdx - 1]); canBeReuse = checkCanReuseNode(rightOldNode, newChildren[rightIdx - 1], rightIdx - 1);
// 不能复用break // 不能复用break
if (!canBeReuse) { if (!canBeReuse) {
break; break;

View File

@ -1,21 +1,16 @@
import type { VNode } from '../Types'; import type { VNode } from '../Types';
import { import { ContextProvider, DomComponent, DomPortal, TreeRoot, SuspenseComponent } from '../vnode/VNodeTags';
ContextProvider,
DomComponent,
DomPortal,
TreeRoot,
SuspenseComponent,
} from '../vnode/VNodeTags';
import { setContext, setNamespaceCtx } from '../ContextSaver'; import { setContext, setNamespaceCtx } from '../ContextSaver';
import { FlagUtils } from '../vnode/VNodeFlags'; import { FlagUtils } from '../vnode/VNodeFlags';
import { onlyUpdateChildVNodes } from '../vnode/VNodeCreator'; import { onlyUpdateChildVNodes } from '../vnode/VNodeCreator';
import componentRenders from './index'; import componentRenders from './index';
import { setProcessingVNode } from '../GlobalVar'; import { setProcessingVNode } from '../GlobalVar';
import { clearVNodeObservers } from '../../horizonx/store/StoreHandler'; import { clearVNodeObservers } from '../../horizonx/store/StoreHandler';
import { pushCurrentRoot } from '../RootStack';
// 复用vNode时也需对stack进行处理 // 复用vNode时也需对树的上下文值处理如contextportal, namespaceContext
function handlerContext(processing: VNode) { function setTreeContextValue(processing: VNode) {
switch (processing.tag) { switch (processing.tag) {
case TreeRoot: case TreeRoot:
setNamespaceCtx(processing, processing.realNode); setNamespaceCtx(processing, processing.realNode);
@ -25,6 +20,7 @@ function handlerContext(processing: VNode) {
break; break;
case DomPortal: case DomPortal:
setNamespaceCtx(processing, processing.realNode); setNamespaceCtx(processing, processing.realNode);
pushCurrentRoot(processing);
break; break;
case ContextProvider: { case ContextProvider: {
const newValue = processing.props.value; const newValue = processing.props.value;
@ -40,13 +36,9 @@ export function captureVNode(processing: VNode): VNode | null {
if (processing.tag !== SuspenseComponent) { if (processing.tag !== SuspenseComponent) {
// 该vNode没有变化不用进入capture直接复用。 // 该vNode没有变化不用进入capture直接复用。
if ( if (!processing.isCreated && processing.oldProps === processing.props && !processing.shouldUpdate) {
!processing.isCreated &&
processing.oldProps === processing.props &&
!processing.shouldUpdate
) {
// 复用还需对stack进行处理 // 复用还需对stack进行处理
handlerContext(processing); setTreeContextValue(processing);
return onlyUpdateChildVNodes(processing); return onlyUpdateChildVNodes(processing);
} }

View File

@ -1,18 +1,16 @@
import type { VNode } from '../Types'; import type { VNode } from '../Types';
import { resetNamespaceCtx, setNamespaceCtx } from '../ContextSaver'; import { resetNamespaceCtx, setNamespaceCtx } from '../ContextSaver';
import { createChildrenByDiff } from '../diff/nodeDiffComparator'; import { createChildrenByDiff } from '../diff/nodeDiffComparator';
import { prePortal } from '../../dom/DOMOperator'; import { popCurrentRoot, pushCurrentRoot } from '../RootStack';
export function bubbleRender(processing: VNode) { export function bubbleRender(processing: VNode) {
resetNamespaceCtx(processing); resetNamespaceCtx(processing);
popCurrentRoot();
if (processing.isCreated) {
prePortal(processing.realNode);
}
} }
function capturePortalComponent(processing: VNode) { function capturePortalComponent(processing: VNode) {
setNamespaceCtx(processing, processing.realNode); setNamespaceCtx(processing, processing.realNode);
pushCurrentRoot(processing);
const newElements = processing.props; const newElements = processing.props;
if (processing.isCreated) { if (processing.isCreated) {

View File

@ -39,6 +39,8 @@ export class VNode {
ref: RefType | ((handle: any) => void) | null = null; // 包裹一个函数submit阶段使用比如将外部useRef生成的对象赋值到ref上 ref: RefType | ((handle: any) => void) | null = null; // 包裹一个函数submit阶段使用比如将外部useRef生成的对象赋值到ref上
oldProps: any = null; oldProps: any = null;
// 是否已经被从树上移除
isCleared = false;
changeList: any; // DOM的变更列表 changeList: any; // DOM的变更列表
effectList: any[] | null; // useEffect 的更新数组 effectList: any[] | null; // useEffect 的更新数组
updates: any[] | null; // TreeRoot和ClassComponent使用的更新数组 updates: any[] | null; // TreeRoot和ClassComponent使用的更新数组
@ -78,7 +80,6 @@ export class VNode {
// 根节点数据 // 根节点数据
toUpdateNodes: Set<VNode> | null; // 保存要更新的节点 toUpdateNodes: Set<VNode> | null; // 保存要更新的节点
delegatedEvents: Set<string>; delegatedEvents: Set<string>;
delegatedNativeEvents: Set<string>;
belongClassVNode: VNode | null = null; // 记录JSXElement所属class vNode处理ref的时候使用 belongClassVNode: VNode | null = null; // 记录JSXElement所属class vNode处理ref的时候使用
@ -100,7 +101,6 @@ export class VNode {
this.task = null; this.task = null;
this.toUpdateNodes = new Set<VNode>(); this.toUpdateNodes = new Set<VNode>();
this.delegatedEvents = new Set<string>(); this.delegatedEvents = new Set<string>();
this.delegatedNativeEvents = new Set<string>();
this.updates = null; this.updates = null;
this.stateCallbacks = null; this.stateCallbacks = null;
this.state = null; this.state = null;
@ -137,6 +137,7 @@ export class VNode {
case DomPortal: case DomPortal:
this.realNode = null; this.realNode = null;
this.context = null; this.context = null;
this.delegatedEvents = new Set<string>();
this.src = null; this.src = null;
break; break;
case DomComponent: case DomComponent:

View File

@ -230,7 +230,7 @@ export function onlyUpdateChildVNodes(processing: VNode): VNode | null {
} }
}; };
putChildrenIntoQueue(processing.child); putChildrenIntoQueue(processing);
while (queue.length) { while (queue.length) {
const vNode = queue.shift()!; const vNode = queue.shift()!;

View File

@ -73,6 +73,7 @@ export function travelVNodeTree(
// 置空vNode // 置空vNode
export function clearVNode(vNode: VNode) { export function clearVNode(vNode: VNode) {
vNode.isCleared = true;
vNode.child = null; vNode.child = null;
vNode.next = null; vNode.next = null;
vNode.depContexts = null; vNode.depContexts = null;

View File

@ -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 ? <C /> : null}
{current === 2 ? <C /> : null}
{current === 3 ? <C /> : null}
</>
);
}
Horizon.render(<App text="app" />, container);
expect(fn).toHaveBeenCalledTimes(1);
update(2);
expect(fn).toHaveBeenCalledTimes(2);
update(3);
expect(fn).toHaveBeenCalledTimes(3);
update(1);
expect(fn).toHaveBeenCalledTimes(4);
});
});

View File

@ -81,7 +81,7 @@ describe('useEffect Hook Test', () => {
expect(LogUtils.getAndClear()).toEqual([]); expect(LogUtils.getAndClear()).toEqual([]);
// 在执行新的render前会执行完上一次render的useEffect所以LogUtils会加入'NewApp effect'。 // 在执行新的render前会执行完上一次render的useEffect所以LogUtils会加入'NewApp effect'。
Horizon.render([na], container); Horizon.render([na], container);
expect(LogUtils.getAndClear()).toEqual(['NewApp effect']); expect(LogUtils.getAndClear()).toEqual(['NewApp effect', 'NewApp']);
expect(container.textContent).toBe('NewApp'); expect(container.textContent).toBe('NewApp');
expect(LogUtils.getAndClear()).toEqual([]); expect(LogUtils.getAndClear()).toEqual([]);
}); });

View File

@ -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 <div></div>;
}
const MemoChild = Horizon.memo(Child);
function App() {
return (
<div>
<MemoChild />
</div>
);
}
const MemoApp = Horizon.memo(App);
Horizon.render(
<div>
<MemoApp key="1" />
</div>,
container
);
Horizon.render(
<div>
<span></span>
<MemoApp key="1" />
</div>,
container
);
expect(() => updateApp()).not.toThrow();
});
});

View File

@ -1,5 +1,6 @@
import * as Horizon from '@cloudsop/horizon/index.ts'; import * as Horizon from '@cloudsop/horizon/index.ts';
import { getLogUtils } from '../jest/testUtils'; import { getLogUtils } from '../jest/testUtils';
import dispatchChangeEvent from '../utils/dispatchChangeEvent';
describe('PortalComponent Test', () => { describe('PortalComponent Test', () => {
const LogUtils = getLogUtils(); const LogUtils = getLogUtils();
@ -14,12 +15,10 @@ describe('PortalComponent Test', () => {
} }
render() { render() {
return Horizon.createPortal( return Horizon.createPortal(this.props.child, this.element);
this.props.child,
this.element,
);
} }
} }
Horizon.render(<PortalApp child={<div>PortalApp</div>} />, container); Horizon.render(<PortalApp child={<div>PortalApp</div>} />, container);
expect(container.textContent).toBe(''); expect(container.textContent).toBe('');
// <div>PortalApp</div>被渲染到了portalRoot而非container // <div>PortalApp</div>被渲染到了portalRoot而非container
@ -43,17 +42,12 @@ describe('PortalComponent Test', () => {
render() { render() {
return [ return [
Horizon.createPortal( Horizon.createPortal(this.props.child, this.element),
this.props.child, Horizon.createPortal(this.props.child, this.newElement),
this.element,
),
Horizon.createPortal(
this.props.child,
this.newElement,
)
]; ];
} }
} }
Horizon.render(<PortalApp child={<div>PortalApp</div>} />, container); Horizon.render(<PortalApp child={<div>PortalApp</div>} />, container);
expect(container.textContent).toBe(''); expect(container.textContent).toBe('');
// <div>PortalApp</div>被渲染到了portalRoot而非container // <div>PortalApp</div>被渲染到了portalRoot而非container
@ -82,21 +76,16 @@ describe('PortalComponent Test', () => {
render() { render() {
return [ return [
<div>PortalApp1st</div>, <div>PortalApp1st</div>,
Horizon.createPortal([
<div>PortalApp4</div>,
Horizon.createPortal( Horizon.createPortal(
this.props.child, [<div>PortalApp4</div>, Horizon.createPortal(this.props.child, this.element3rd)],
this.element3rd, this.element
), ),
], this.element),
<div>PortalApp2nd</div>, <div>PortalApp2nd</div>,
Horizon.createPortal( Horizon.createPortal(this.props.child, this.newElement),
this.props.child,
this.newElement,
)
]; ];
} }
} }
Horizon.render(<PortalApp child={<div>PortalApp</div>} />, container); Horizon.render(<PortalApp child={<div>PortalApp</div>} />, container);
expect(container.textContent).toBe('PortalApp1stPortalApp2nd'); expect(container.textContent).toBe('PortalApp1stPortalApp2nd');
// <div>PortalApp4</div>会挂载在this.element上 // <div>PortalApp4</div>会挂载在this.element上
@ -120,25 +109,23 @@ describe('PortalComponent Test', () => {
} }
render() { render() {
return Horizon.createPortal( return Horizon.createPortal(this.props.child, this.element);
this.props.child,
this.element,
);
} }
} }
Horizon.render(<PortalApp key='portal' child={<div>PortalApp</div>} />, container);
Horizon.render(<PortalApp key="portal" child={<div>PortalApp</div>} />, container);
expect(container.textContent).toBe(''); expect(container.textContent).toBe('');
expect(portalRoot.textContent).toBe('PortalApp'); expect(portalRoot.textContent).toBe('PortalApp');
Horizon.render(<PortalApp key='portal' child={<div>AppPortal</div>} />, container); Horizon.render(<PortalApp key="portal" child={<div>AppPortal</div>} />, container);
expect(container.textContent).toBe(''); expect(container.textContent).toBe('');
expect(portalRoot.textContent).toBe('AppPortal'); expect(portalRoot.textContent).toBe('AppPortal');
Horizon.render(<PortalApp key='portal' child={['por', 'tal']} />, container); Horizon.render(<PortalApp key="portal" child={['por', 'tal']} />, container);
expect(container.textContent).toBe(''); expect(container.textContent).toBe('');
expect(portalRoot.textContent).toBe('portal'); expect(portalRoot.textContent).toBe('portal');
Horizon.render(<PortalApp key='portal' child={null} />, container); Horizon.render(<PortalApp key="portal" child={null} />, container);
expect(container.textContent).toBe(''); expect(container.textContent).toBe('');
expect(portalRoot.textContent).toBe(''); expect(portalRoot.textContent).toBe('');
@ -158,10 +145,7 @@ describe('PortalComponent Test', () => {
} }
render() { render() {
return Horizon.createPortal( return Horizon.createPortal(this.props.child, this.element);
this.props.child,
this.element,
);
} }
} }
@ -173,7 +157,6 @@ describe('PortalComponent Test', () => {
); );
}; };
const App = () => { const App = () => {
const handleClick = () => { const handleClick = () => {
LogUtils.log('bubble click event'); LogUtils.log('bubble click event');
@ -185,9 +168,7 @@ describe('PortalComponent Test', () => {
return ( return (
<div onClickCapture={handleCaptureClick()} onClick={handleClick()}> <div onClickCapture={handleCaptureClick()} onClick={handleClick()}>
<PortalApp child={<Child />}> <PortalApp child={<Child />}></PortalApp>
</PortalApp>
</div> </div>
); );
}; };
@ -199,7 +180,95 @@ describe('PortalComponent Test', () => {
expect(LogUtils.getAndClear()).toEqual([ expect(LogUtils.getAndClear()).toEqual([
// 从外到内先捕获再冒泡 // 从外到内先捕获再冒泡
'capture click event', 'capture click event',
'bubble click event' 'bubble click event',
]); ]);
}); });
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 (
<div>
<button onClick={onClick} ref={btnRef}></button>
<PortalApp />
</div>
);
}
}
Horizon.render(<App />, 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 <input onChange={fn} ref={inputRef} />;
};
return (
<div>
<Dialog>
<Input />
</Dialog>
</div>
);
}
Horizon.render(<App />, container);
showPortalInput(true);
jest.advanceTimersToNextTimer();
dispatchChangeEvent(inputRef.current, 'test');
expect(fn).toHaveBeenCalledTimes(1);
});
}); });

View File

@ -1,24 +1,24 @@
import { createStore } from "@cloudsop/horizon/src/horizonx/store/StoreHandler"; import { createStore } from '@cloudsop/horizon/src/horizonx/store/StoreHandler';
import { watch } from "@cloudsop/horizon/src/horizonx/proxy/watch"; import { watch } from '@cloudsop/horizon/src/horizonx/proxy/watch';
describe("watch",()=>{ describe('watch', () => {
it('shouhld watch promitive state variable', async () => { it('shouhld watch promitive state variable', async () => {
const useStore = createStore({ const useStore = createStore({
state: { state: {
variable:'x' variable: 'x',
}, },
actions: { actions: {
change:(state)=>state.variable = "a" change: state => (state.variable = 'a'),
} },
}); });
const store = useStore(); const store = useStore();
let counter = 0; let counter = 0;
watch(store.$s,(state)=>{ watch(store.$s, state => {
counter++; counter++;
expect(state.variable).toBe('a'); expect(state.variable).toBe('a');
}) });
store.change(); store.change();
@ -27,11 +27,11 @@ describe("watch",()=>{
it('shouhld watch object variable', async () => { it('shouhld watch object variable', async () => {
const useStore = createStore({ const useStore = createStore({
state: { state: {
variable:'x' variable: 'x',
}, },
actions: { actions: {
change:(state)=>state.variable = "a" change: state => (state.variable = 'a'),
} },
}); });
const store = useStore(); const store = useStore();
@ -39,7 +39,7 @@ describe("watch",()=>{
store.$s.watch('variable', () => { store.$s.watch('variable', () => {
counter++; counter++;
}) });
store.change(); store.change();
@ -49,11 +49,11 @@ describe("watch",()=>{
it('shouhld watch array item', async () => { it('shouhld watch array item', async () => {
const useStore = createStore({ const useStore = createStore({
state: { state: {
arr:['x'] arr: ['x'],
}, },
actions: { actions: {
change:(state)=>state.arr[0]='a' change: state => (state.arr[0] = 'a'),
} },
}); });
const store = useStore(); const store = useStore();
@ -61,7 +61,7 @@ describe("watch",()=>{
store.arr.watch('0', () => { store.arr.watch('0', () => {
counter++; counter++;
}) });
store.change(); store.change();
@ -71,13 +71,11 @@ describe("watch",()=>{
it('shouhld watch collection item', async () => { it('shouhld watch collection item', async () => {
const useStore = createStore({ const useStore = createStore({
state: { state: {
collection:new Map([ collection: new Map([['a', 'a']]),
['a', 'a'],
])
}, },
actions: { actions: {
change:(state)=>state.collection.set('a','x') change: state => state.collection.set('a', 'x'),
} },
}); });
const store = useStore(); const store = useStore();
@ -85,7 +83,7 @@ describe("watch",()=>{
store.collection.watch('a', () => { store.collection.watch('a', () => {
counter++; counter++;
}) });
store.change(); store.change();
@ -96,12 +94,12 @@ describe("watch",()=>{
const useStore = createStore({ const useStore = createStore({
state: { state: {
bool1: true, bool1: true,
bool2:false bool2: false,
}, },
actions: { actions: {
toggle1:state=>state.bool1=!state.bool1, toggle1: state => (state.bool1 = !state.bool1),
toggle2:state=>state.bool2=!state.bool2 toggle2: state => (state.bool2 = !state.bool2),
} },
}); });
let counter1 = 0; let counter1 = 0;
@ -110,7 +108,7 @@ describe("watch",()=>{
watch(store.$s, () => { watch(store.$s, () => {
counterAll++; counterAll++;
}) });
store.$s.watch('bool1', () => { store.$s.watch('bool1', () => {
counter1++; counter1++;
@ -128,5 +126,5 @@ describe("watch",()=>{
expect(counter1).toBe(3); expect(counter1).toBe(3);
expect(counterAll).toBe(6); expect(counterAll).toBe(6);
}) });
}) });

View File

@ -8,18 +8,18 @@ const { unmountComponentAtNode } = Horizon;
const useStore1 = createStore({ const useStore1 = createStore({
state: { counter: 1 }, state: { counter: 1 },
actions: { actions: {
add:(state)=>state.counter++, add: state => state.counter++,
reset: (state)=>state.counter=1 reset: state => (state.counter = 1),
} },
}) });
const useStore2 = createStore({ const useStore2 = createStore({
state: { counter2: 1 }, state: { counter2: 1 },
actions: { actions: {
add2:(state)=>state.counter2++, add2: state => state.counter2++,
reset: (state)=>state.counter2=1 reset: state => (state.counter2 = 1),
} },
}) });
describe('Using multiple stores', () => { describe('Using multiple stores', () => {
let container: HTMLElement | null = null; let container: HTMLElement | null = null;
@ -65,9 +65,11 @@ describe('Using multiple stores', () => {
> >
add add
</button> </button>
<p id={RESULT_ID}>{counter} {counter2}</p> <p id={RESULT_ID}>
{counter} {counter2}
</p>
</div> </div>
) );
} }
} }
@ -76,28 +78,25 @@ describe('Using multiple stores', () => {
expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('1 1'); expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('1 1');
Horizon.act(() => { Horizon.act(() => {
triggerClickEvent(container, BUTTON_ID); triggerClickEvent(container, BUTTON_ID);
}); });
expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('2 1'); expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('2 1');
Horizon.act(() => { Horizon.act(() => {
triggerClickEvent(container, BUTTON_ID2); triggerClickEvent(container, BUTTON_ID2);
}); });
expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('2 2'); expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('2 2');
}); });
it('Should use use stores in cycles and multiple methods', () => { it('Should use use stores in cycles and multiple methods', () => {
interface App { interface App {
store:any, store: any;
store2:any store2: any;
} }
class App extends Horizon.Component { class App extends Horizon.Component {
constructor() { constructor() {
super(); super();
this.store = useStore1(); this.store = useStore1();
this.store2 = useStore2() this.store2 = useStore2();
} }
render() { render() {
@ -129,9 +128,11 @@ describe('Using multiple stores', () => {
> >
add add
</button> </button>
<p id={RESULT_ID}>{counter} {counter2}</p> <p id={RESULT_ID}>
{counter} {counter2}
</p>
</div> </div>
) );
} }
} }
@ -140,16 +141,13 @@ describe('Using multiple stores', () => {
expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('1 1'); expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('1 1');
Horizon.act(() => { Horizon.act(() => {
triggerClickEvent(container, BUTTON_ID); triggerClickEvent(container, BUTTON_ID);
}); });
expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('2 1'); expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('2 1');
Horizon.act(() => { Horizon.act(() => {
triggerClickEvent(container, BUTTON_ID2); triggerClickEvent(container, BUTTON_ID2);
}); });
expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('2 2'); expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('2 2');
}); });
it('Should use multiple stores in function component', () => { it('Should use multiple stores in function component', () => {
@ -176,7 +174,9 @@ describe('Using multiple stores', () => {
> >
add add
</button> </button>
<p id={RESULT_ID}>{counter} {counter2}</p> <p id={RESULT_ID}>
{counter} {counter2}
</p>
</div> </div>
); );
} }
@ -186,13 +186,11 @@ describe('Using multiple stores', () => {
expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('1 1'); expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('1 1');
Horizon.act(() => { Horizon.act(() => {
triggerClickEvent(container, BUTTON_ID); triggerClickEvent(container, BUTTON_ID);
}); });
expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('2 1'); expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('2 1');
Horizon.act(() => { Horizon.act(() => {
triggerClickEvent(container, BUTTON_ID2); triggerClickEvent(container, BUTTON_ID2);
}); });
expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('2 2'); expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('2 2');
}); });

View File

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