From 1d7c9ff675efd3377b7b8a6bf62086dab943525b Mon Sep 17 00:00:00 2001 From: * <*> Date: Fri, 12 Jan 2024 20:14:00 +0800 Subject: [PATCH 1/2] Match-id-b04ce71082fed519500c8c15e4c2bccabe4ecc04 --- packages/inula-reactive/.editorconfig | 10 + packages/inula-reactive/.gitignore | 6 + packages/inula-reactive/README.md | 174 +++ packages/inula-reactive/babel.config.js | 49 + packages/inula-reactive/global.d.ts | 23 + packages/inula-reactive/jest.config.js | 36 + packages/inula-reactive/npm/index.js | 22 + packages/inula-reactive/package.json | 26 + .../scripts/__tests__/ActTest/act.test.js | 53 + .../__tests__/ComponentTest/ClassRefs.test.js | 51 + .../ComponentTest/ComponentError.test.js | 54 + .../__tests__/ComponentTest/Context.test.js | 414 ++++++ .../ComponentTest/DiffAlgorithm.test.js | 59 + .../ComponentTest/ForwardRef.test.js | 60 + .../ComponentTest/FragmentComponent.test.js | 476 +++++++ .../ComponentTest/FunctionComponent.test.js | 92 ++ .../HookTest/UseCallback.test.js | 49 + .../ComponentTest/HookTest/UseContext.test.js | 93 ++ .../ComponentTest/HookTest/UseEffect.test.js | 681 ++++++++++ .../HookTest/UseImperativeHandle.test.js | 97 ++ .../HookTest/UseLayoutEffect.test.js | 118 ++ .../ComponentTest/HookTest/UseMemo.test.js | 112 ++ .../ComponentTest/HookTest/UseReducer.test.js | 104 ++ .../ComponentTest/HookTest/UseRef.test.js | 73 + .../ComponentTest/HookTest/UseState.test.js | 178 +++ .../ComponentTest/JsxElement.test.js | 42 + .../ComponentTest/LazyComponent.test.js | 223 ++++ .../__tests__/ComponentTest/LifeCycle.test.js | 467 +++++++ .../__tests__/ComponentTest/Memo.test.js | 52 + .../ComponentTest/PortalComponent.test.js | 289 ++++ .../ComponentTest/SuspenseComponent.test.js | 73 + .../__tests__/DomTest/Attribute.test.js | 98 ++ .../__tests__/DomTest/DomInput.test.js | 362 +++++ .../__tests__/DomTest/DomSelect.test.js | 336 +++++ .../__tests__/DomTest/DomTextarea.test.js | 136 ++ .../__tests__/EventTest/EventMain.test.js | 281 ++++ .../__tests__/EventTest/FocusEvent.test.js | 59 + .../__tests__/EventTest/KeyboardEvent.test.js | 130 ++ .../EventTest/MouseEnterEvent.test.js | 278 ++++ .../__tests__/EventTest/MouseEvent.test.js | 185 +++ .../__tests__/EventTest/WheelEvent.test.js | 62 + .../StateManager/StateArray.test.tsx | 223 ++++ .../StateManager/StateMap.test.tsx | 348 +++++ .../StateManager/StateMixType.test.tsx | 177 +++ .../StateManager/StateSet.test.tsx | 318 +++++ .../StateManager/StateWeakMap.test.tsx | 149 +++ .../StateManager/StateWeakSet.test.tsx | 120 ++ .../StoreFunctionality/async.test.tsx | 112 ++ .../StoreFunctionality/basicAccess.test.tsx | 198 +++ .../StoreFunctionality/cloneDeep.test.js | 120 ++ .../StoreFunctionality/dollarAccess.test.tsx | 80 ++ .../StoreFunctionality/otherCases.test.tsx | 165 +++ .../HorizonXTest/StoreFunctionality/reset.js | 104 ++ .../HorizonXTest/StoreFunctionality/store.ts | 40 + .../StoreFunctionality/utils.test.js | 108 ++ .../StoreFunctionality/watch.test.tsx | 145 ++ .../adapters/ReduxAdapter.test.tsx | 234 ++++ .../adapters/ReduxAdapterThunk.test.tsx | 54 + .../adapters/ReduxReactAdapter.test.tsx | 378 ++++++ .../HorizonXTest/adapters/connectTest.tsx | 61 + .../class/ClassException.test.tsx | 89 ++ .../class/ClassStateArray.test.tsx | 248 ++++ .../HorizonXTest/class/ClassStateMap.test.tsx | 370 ++++++ .../clear/ClassVNodeClear.test.tsx | 137 ++ .../clear/FunctionVNodeClear.test.tsx | 132 ++ .../edgeCases/deepVariableObserver.test.tsx | 170 +++ .../edgeCases/multipleStores.test.tsx | 212 +++ .../HorizonXTest/edgeCases/proxy.test.tsx | 63 + .../__tests__/InulaIsTest/index.test.js | 71 + .../For/reactive-component-for-add.test.js | 162 +++ .../For/reactive-component-for-delete.test.js | 129 ++ .../For/reactive-component-for-update.test.js | 1172 +++++++++++++++++ .../For/reactive-component-for.test.js | 251 ++++ .../reactive-component-block.test.js | 94 ++ .../reactive-component-combination.test.js | 105 ++ .../reactive-component-rtext.test.js | 44 + .../component/reactive-component-show.test.js | 200 +++ .../reactive-component-switch.test.js | 75 ++ .../__tests__/ReactivityTest/computed.test.js | 51 + .../ReactivityTest/reactive-compute.test.js | 377 ++++++ .../ReactivityTest/reactive-memory.test.js | 77 ++ .../ReactivityTest/reactive-mix-use.test.js | 60 + .../reactive-mixed-children.test.js | 64 + .../ReactivityTest/reactive-object.test.js | 973 ++++++++++++++ .../ReactivityTest/reactive-primitive.test.js | 266 ++++ .../reactive-props-update.test.js | 96 ++ .../ReactivityTest/reactive-watch.test.js | 122 ++ .../__tests__/jest/commonComponents.js | 44 + .../scripts/__tests__/jest/jestEnvironment.js | 23 + .../scripts/__tests__/jest/jestSetting.js | 59 + .../scripts/__tests__/jest/logUtils.js | 41 + .../scripts/__tests__/jest/testUtils.js | 66 + .../__tests__/utils/dispatchChangeEvent.js | 20 + packages/inula-reactive/scripts/gen3rdLib.js | 39 + .../scripts/rollup/build-types.js | 65 + .../scripts/rollup/copy-plugin.js | 28 + .../scripts/rollup/rollup.config.js | 130 ++ packages/inula-reactive/src/EventTypes.ts | 27 + .../inula-reactive/src/dom/DOMExternal.ts | 146 ++ .../inula-reactive/src/dom/DOMInternalKeys.ts | 87 ++ .../inula-reactive/src/dom/DOMOperator.ts | 224 ++++ .../DOMPropertiesHandler.ts | 202 +++ .../dom/DOMPropertiesHandler/StyleHandler.ts | 100 ++ .../DOMPropertiesHandler/UpdateCommonProp.ts | 155 +++ .../src/dom/SelectionRangeHandler.ts | 184 +++ .../inula-reactive/src/dom/utils/Common.ts | 90 ++ .../src/dom/utils/DomCreator.ts | 34 + .../inula-reactive/src/dom/utils/Interface.ts | 24 + .../src/dom/validators/PropertiesData.ts | 142 ++ .../src/dom/validators/ValidateProps.ts | 121 ++ .../src/dom/valueHandler/InputValueHandler.ts | 80 ++ .../dom/valueHandler/OptionValueHandler.ts | 36 + .../dom/valueHandler/SelectValueHandler.ts | 87 ++ .../dom/valueHandler/TextareaValueHandler.ts | 68 + .../dom/valueHandler/ValueChangeHandler.ts | 93 ++ .../src/dom/valueHandler/index.ts | 77 ++ .../inula-reactive/src/event/EventBinding.ts | 138 ++ packages/inula-reactive/src/event/EventHub.ts | 115 ++ .../inula-reactive/src/event/EventWrapper.ts | 95 ++ .../src/event/FormValueController.ts | 89 ++ .../src/event/InulaEventMain.ts | 198 +++ .../src/event/ListenerGetter.ts | 163 +++ .../inula-reactive/src/event/MouseEvent.ts | 107 ++ packages/inula-reactive/src/event/Types.ts | 32 + packages/inula-reactive/src/event/utils.ts | 49 + .../src/external/ChildrenUtil.ts | 134 ++ .../inula-reactive/src/external/InulaIs.ts | 113 ++ .../inula-reactive/src/external/JSXElement.ts | 144 ++ .../src/external/JSXElementType.ts | 28 + .../inula-reactive/src/external/TestUtil.ts | 78 ++ .../inula-reactive/src/external/devtools.ts | 137 ++ packages/inula-reactive/src/index.ts | 226 ++++ .../inula-reactive/src/inulax/CommonUtils.ts | 174 +++ .../inula-reactive/src/inulax/Constants.ts | 18 + .../src/inulax/adapters/redux.ts | 203 +++ .../src/inulax/adapters/reduxReact.ts | 177 +++ .../src/inulax/adapters/reduxThunk.ts | 39 + .../src/inulax/devtools/constants.ts | 25 + .../src/inulax/devtools/index.ts | 251 ++++ .../src/inulax/proxy/HooklessObserver.ts | 53 + .../src/inulax/proxy/Observer.ts | 160 +++ .../src/inulax/proxy/ProxyHandler.ts | 124 ++ .../proxy/handlers/ArrayProxyHandler.ts | 162 +++ .../proxy/handlers/CollectionProxyHandler.ts | 37 + .../src/inulax/proxy/handlers/MapProxy.ts | 429 ++++++ .../proxy/handlers/ObjectProxyHandler.ts | 130 ++ .../src/inulax/proxy/handlers/SetProxy.ts | 310 +++++ .../src/inulax/proxy/handlers/WeakMapProxy.ts | 209 +++ .../src/inulax/proxy/handlers/WeakSetProxy.ts | 143 ++ .../src/inulax/proxy/readonlyProxy.ts | 43 + .../inula-reactive/src/inulax/proxy/watch.ts | 23 + .../src/inulax/store/StoreHandler.ts | 336 +++++ packages/inula-reactive/src/inulax/types.ts | 98 ++ .../inula-reactive/src/jsx-dev-runtime.ts | 19 + packages/inula-reactive/src/jsx-runtime.ts | 19 + packages/inula-reactive/src/reactive/Atom.ts | 78 ++ packages/inula-reactive/src/reactive/Batch.ts | 92 ++ .../inula-reactive/src/reactive/Computed.ts | 55 + .../inula-reactive/src/reactive/DiffUtils.ts | 199 +++ .../inula-reactive/src/reactive/RContext.ts | 355 +++++ .../src/reactive/RContextCreator.ts | 202 +++ packages/inula-reactive/src/reactive/RNode.ts | 156 +++ .../inula-reactive/src/reactive/Reactive.ts | 21 + packages/inula-reactive/src/reactive/Utils.ts | 89 ++ packages/inula-reactive/src/reactive/Var.ts | 22 + packages/inula-reactive/src/reactive/Watch.ts | 42 + .../src/reactive/components/Block.tsx | 35 + .../src/reactive/components/For.tsx | 91 ++ .../src/reactive/components/RText.ts | 25 + .../src/reactive/components/Show.tsx | 38 + .../src/reactive/components/Switch.ts | 42 + .../src/reactive/proxy/RProxyHandler.ts | 146 ++ packages/inula-reactive/src/reactive/types.ts | 116 ++ .../src/renderer/ContextSaver.ts | 60 + .../src/renderer/ErrorHandler.ts | 197 +++ .../src/renderer/ExecuteMode.ts | 49 + .../inula-reactive/src/renderer/GlobalVar.ts | 58 + .../inula-reactive/src/renderer/Renderer.ts | 45 + .../inula-reactive/src/renderer/RootStack.ts | 34 + .../src/renderer/TreeBuilder.ts | 457 +++++++ packages/inula-reactive/src/renderer/Types.ts | 157 +++ .../src/renderer/UpdateHandler.ts | 117 ++ .../renderer/components/BaseClassComponent.ts | 70 + .../src/renderer/components/CreatePortal.ts | 32 + .../src/renderer/components/CreateRef.ts | 22 + .../src/renderer/components/ForwardRef.ts | 36 + .../src/renderer/components/Lazy.ts | 85 ++ .../src/renderer/components/Memo.ts | 42 + .../renderer/components/context/Context.ts | 48 + .../components/context/CreateContext.ts | 37 + .../src/renderer/diff/DiffTools.ts | 37 + .../src/renderer/diff/nodeDiffComparator.ts | 770 +++++++++++ .../src/renderer/hooks/BaseHook.ts | 86 ++ .../src/renderer/hooks/EffectConstant.ts | 21 + .../src/renderer/hooks/HookExternal.ts | 142 ++ .../src/renderer/hooks/HookMain.ts | 65 + .../src/renderer/hooks/HookStage.ts | 30 + .../src/renderer/hooks/HookType.ts | 70 + .../src/renderer/hooks/UseCallbackHook.ts | 44 + .../src/renderer/hooks/UseEffectHook.ts | 91 ++ .../src/renderer/hooks/UseImperativeHook.ts | 51 + .../src/renderer/hooks/UseMemoHook.ts | 47 + .../src/renderer/hooks/UseReducerHook.ts | 155 +++ .../src/renderer/hooks/UseRefHook.ts | 35 + .../src/renderer/hooks/UseStateHook.ts | 29 + .../src/renderer/hooks/UseWatch.ts | 20 + .../src/renderer/hooks/reactive/UseAtom.ts | 53 + .../src/renderer/hooks/reactive/UseCompute.ts | 24 + .../renderer/hooks/reactive/UseReactive.ts | 49 + .../src/renderer/hooks/reactive/UseWatch.ts | 28 + .../src/renderer/render/BaseComponent.ts | 79 ++ .../src/renderer/render/ClassComponent.ts | 184 +++ .../src/renderer/render/ContextConsumer.ts | 38 + .../src/renderer/render/ContextProvider.ts | 121 ++ .../src/renderer/render/DomComponent.ts | 123 ++ .../src/renderer/render/DomPortal.ts | 43 + .../src/renderer/render/DomText.ts | 49 + .../src/renderer/render/ForwardRef.ts | 23 + .../src/renderer/render/Fragment.ts | 29 + .../src/renderer/render/FunctionComponent.ts | 90 ++ .../src/renderer/render/LazyComponent.ts | 91 ++ .../src/renderer/render/MemoComponent.ts | 76 ++ .../src/renderer/render/ReactiveComponent.ts | 46 + .../src/renderer/render/SuspenseComponent.ts | 247 ++++ .../src/renderer/render/TreeRoot.ts | 55 + .../render/class/ClassLifeCycleProcessor.ts | 163 +++ .../src/renderer/render/index.ts | 66 + .../src/renderer/submit/HookEffectHandler.ts | 156 +++ .../src/renderer/submit/LifeCycleHandler.ts | 584 ++++++++ .../src/renderer/submit/Submit.ts | 218 +++ .../src/renderer/taskExecutor/BrowserAsync.ts | 79 ++ .../src/renderer/taskExecutor/RenderQueue.ts | 73 + .../src/renderer/taskExecutor/TaskExecutor.ts | 110 ++ .../src/renderer/taskExecutor/TaskQueue.ts | 77 ++ .../src/renderer/utils/compare.ts | 66 + .../src/renderer/utils/throwIfTrue.ts | 28 + .../src/renderer/utils/vNodePath.ts | 30 + .../src/renderer/vnode/VNode.ts | 168 +++ .../src/renderer/vnode/VNodeCreator.ts | 269 ++++ .../src/renderer/vnode/VNodeFlags.ts | 106 ++ .../src/renderer/vnode/VNodeShouldUpdate.ts | 83 ++ .../src/renderer/vnode/VNodeTags.ts | 36 + .../src/renderer/vnode/VNodeUtils.ts | 298 +++++ packages/inula-reactive/src/temp.js | 8 + packages/inula-reactive/src/types.ts | 440 +++++++ packages/inula-reactive/tsconfig.build.json | 11 + packages/inula-reactive/tsconfig.json | 38 + .../inula/scripts/__tests__/temp.test.jsx | 308 +++++ 248 files changed, 32553 insertions(+) create mode 100644 packages/inula-reactive/.editorconfig create mode 100644 packages/inula-reactive/.gitignore create mode 100644 packages/inula-reactive/README.md create mode 100644 packages/inula-reactive/babel.config.js create mode 100644 packages/inula-reactive/global.d.ts create mode 100644 packages/inula-reactive/jest.config.js create mode 100644 packages/inula-reactive/npm/index.js create mode 100644 packages/inula-reactive/package.json create mode 100644 packages/inula-reactive/scripts/__tests__/ActTest/act.test.js create mode 100644 packages/inula-reactive/scripts/__tests__/ComponentTest/ClassRefs.test.js create mode 100644 packages/inula-reactive/scripts/__tests__/ComponentTest/ComponentError.test.js create mode 100644 packages/inula-reactive/scripts/__tests__/ComponentTest/Context.test.js create mode 100644 packages/inula-reactive/scripts/__tests__/ComponentTest/DiffAlgorithm.test.js create mode 100644 packages/inula-reactive/scripts/__tests__/ComponentTest/ForwardRef.test.js create mode 100644 packages/inula-reactive/scripts/__tests__/ComponentTest/FragmentComponent.test.js create mode 100644 packages/inula-reactive/scripts/__tests__/ComponentTest/FunctionComponent.test.js create mode 100644 packages/inula-reactive/scripts/__tests__/ComponentTest/HookTest/UseCallback.test.js create mode 100644 packages/inula-reactive/scripts/__tests__/ComponentTest/HookTest/UseContext.test.js create mode 100644 packages/inula-reactive/scripts/__tests__/ComponentTest/HookTest/UseEffect.test.js create mode 100644 packages/inula-reactive/scripts/__tests__/ComponentTest/HookTest/UseImperativeHandle.test.js create mode 100644 packages/inula-reactive/scripts/__tests__/ComponentTest/HookTest/UseLayoutEffect.test.js create mode 100644 packages/inula-reactive/scripts/__tests__/ComponentTest/HookTest/UseMemo.test.js create mode 100644 packages/inula-reactive/scripts/__tests__/ComponentTest/HookTest/UseReducer.test.js create mode 100644 packages/inula-reactive/scripts/__tests__/ComponentTest/HookTest/UseRef.test.js create mode 100644 packages/inula-reactive/scripts/__tests__/ComponentTest/HookTest/UseState.test.js create mode 100644 packages/inula-reactive/scripts/__tests__/ComponentTest/JsxElement.test.js create mode 100644 packages/inula-reactive/scripts/__tests__/ComponentTest/LazyComponent.test.js create mode 100644 packages/inula-reactive/scripts/__tests__/ComponentTest/LifeCycle.test.js create mode 100644 packages/inula-reactive/scripts/__tests__/ComponentTest/Memo.test.js create mode 100644 packages/inula-reactive/scripts/__tests__/ComponentTest/PortalComponent.test.js create mode 100644 packages/inula-reactive/scripts/__tests__/ComponentTest/SuspenseComponent.test.js create mode 100644 packages/inula-reactive/scripts/__tests__/DomTest/Attribute.test.js create mode 100644 packages/inula-reactive/scripts/__tests__/DomTest/DomInput.test.js create mode 100644 packages/inula-reactive/scripts/__tests__/DomTest/DomSelect.test.js create mode 100644 packages/inula-reactive/scripts/__tests__/DomTest/DomTextarea.test.js create mode 100644 packages/inula-reactive/scripts/__tests__/EventTest/EventMain.test.js create mode 100644 packages/inula-reactive/scripts/__tests__/EventTest/FocusEvent.test.js create mode 100644 packages/inula-reactive/scripts/__tests__/EventTest/KeyboardEvent.test.js create mode 100644 packages/inula-reactive/scripts/__tests__/EventTest/MouseEnterEvent.test.js create mode 100644 packages/inula-reactive/scripts/__tests__/EventTest/MouseEvent.test.js create mode 100644 packages/inula-reactive/scripts/__tests__/EventTest/WheelEvent.test.js create mode 100644 packages/inula-reactive/scripts/__tests__/HorizonXTest/StateManager/StateArray.test.tsx create mode 100644 packages/inula-reactive/scripts/__tests__/HorizonXTest/StateManager/StateMap.test.tsx create mode 100644 packages/inula-reactive/scripts/__tests__/HorizonXTest/StateManager/StateMixType.test.tsx create mode 100644 packages/inula-reactive/scripts/__tests__/HorizonXTest/StateManager/StateSet.test.tsx create mode 100644 packages/inula-reactive/scripts/__tests__/HorizonXTest/StateManager/StateWeakMap.test.tsx create mode 100644 packages/inula-reactive/scripts/__tests__/HorizonXTest/StateManager/StateWeakSet.test.tsx create mode 100644 packages/inula-reactive/scripts/__tests__/HorizonXTest/StoreFunctionality/async.test.tsx create mode 100644 packages/inula-reactive/scripts/__tests__/HorizonXTest/StoreFunctionality/basicAccess.test.tsx create mode 100644 packages/inula-reactive/scripts/__tests__/HorizonXTest/StoreFunctionality/cloneDeep.test.js create mode 100644 packages/inula-reactive/scripts/__tests__/HorizonXTest/StoreFunctionality/dollarAccess.test.tsx create mode 100644 packages/inula-reactive/scripts/__tests__/HorizonXTest/StoreFunctionality/otherCases.test.tsx create mode 100644 packages/inula-reactive/scripts/__tests__/HorizonXTest/StoreFunctionality/reset.js create mode 100644 packages/inula-reactive/scripts/__tests__/HorizonXTest/StoreFunctionality/store.ts create mode 100644 packages/inula-reactive/scripts/__tests__/HorizonXTest/StoreFunctionality/utils.test.js create mode 100644 packages/inula-reactive/scripts/__tests__/HorizonXTest/StoreFunctionality/watch.test.tsx create mode 100644 packages/inula-reactive/scripts/__tests__/HorizonXTest/adapters/ReduxAdapter.test.tsx create mode 100644 packages/inula-reactive/scripts/__tests__/HorizonXTest/adapters/ReduxAdapterThunk.test.tsx create mode 100644 packages/inula-reactive/scripts/__tests__/HorizonXTest/adapters/ReduxReactAdapter.test.tsx create mode 100644 packages/inula-reactive/scripts/__tests__/HorizonXTest/adapters/connectTest.tsx create mode 100644 packages/inula-reactive/scripts/__tests__/HorizonXTest/class/ClassException.test.tsx create mode 100644 packages/inula-reactive/scripts/__tests__/HorizonXTest/class/ClassStateArray.test.tsx create mode 100644 packages/inula-reactive/scripts/__tests__/HorizonXTest/class/ClassStateMap.test.tsx create mode 100644 packages/inula-reactive/scripts/__tests__/HorizonXTest/clear/ClassVNodeClear.test.tsx create mode 100644 packages/inula-reactive/scripts/__tests__/HorizonXTest/clear/FunctionVNodeClear.test.tsx create mode 100644 packages/inula-reactive/scripts/__tests__/HorizonXTest/edgeCases/deepVariableObserver.test.tsx create mode 100644 packages/inula-reactive/scripts/__tests__/HorizonXTest/edgeCases/multipleStores.test.tsx create mode 100644 packages/inula-reactive/scripts/__tests__/HorizonXTest/edgeCases/proxy.test.tsx create mode 100644 packages/inula-reactive/scripts/__tests__/InulaIsTest/index.test.js create mode 100644 packages/inula-reactive/scripts/__tests__/ReactivityTest/component/For/reactive-component-for-add.test.js create mode 100644 packages/inula-reactive/scripts/__tests__/ReactivityTest/component/For/reactive-component-for-delete.test.js create mode 100644 packages/inula-reactive/scripts/__tests__/ReactivityTest/component/For/reactive-component-for-update.test.js create mode 100644 packages/inula-reactive/scripts/__tests__/ReactivityTest/component/For/reactive-component-for.test.js create mode 100644 packages/inula-reactive/scripts/__tests__/ReactivityTest/component/reactive-component-block.test.js create mode 100644 packages/inula-reactive/scripts/__tests__/ReactivityTest/component/reactive-component-combination.test.js create mode 100644 packages/inula-reactive/scripts/__tests__/ReactivityTest/component/reactive-component-rtext.test.js create mode 100644 packages/inula-reactive/scripts/__tests__/ReactivityTest/component/reactive-component-show.test.js create mode 100644 packages/inula-reactive/scripts/__tests__/ReactivityTest/component/reactive-component-switch.test.js create mode 100644 packages/inula-reactive/scripts/__tests__/ReactivityTest/computed.test.js create mode 100644 packages/inula-reactive/scripts/__tests__/ReactivityTest/reactive-compute.test.js create mode 100644 packages/inula-reactive/scripts/__tests__/ReactivityTest/reactive-memory.test.js create mode 100644 packages/inula-reactive/scripts/__tests__/ReactivityTest/reactive-mix-use.test.js create mode 100644 packages/inula-reactive/scripts/__tests__/ReactivityTest/reactive-mixed-children.test.js create mode 100644 packages/inula-reactive/scripts/__tests__/ReactivityTest/reactive-object.test.js create mode 100644 packages/inula-reactive/scripts/__tests__/ReactivityTest/reactive-primitive.test.js create mode 100644 packages/inula-reactive/scripts/__tests__/ReactivityTest/reactive-props-update.test.js create mode 100644 packages/inula-reactive/scripts/__tests__/ReactivityTest/reactive-watch.test.js create mode 100644 packages/inula-reactive/scripts/__tests__/jest/commonComponents.js create mode 100644 packages/inula-reactive/scripts/__tests__/jest/jestEnvironment.js create mode 100644 packages/inula-reactive/scripts/__tests__/jest/jestSetting.js create mode 100644 packages/inula-reactive/scripts/__tests__/jest/logUtils.js create mode 100644 packages/inula-reactive/scripts/__tests__/jest/testUtils.js create mode 100644 packages/inula-reactive/scripts/__tests__/utils/dispatchChangeEvent.js create mode 100644 packages/inula-reactive/scripts/gen3rdLib.js create mode 100644 packages/inula-reactive/scripts/rollup/build-types.js create mode 100644 packages/inula-reactive/scripts/rollup/copy-plugin.js create mode 100644 packages/inula-reactive/scripts/rollup/rollup.config.js create mode 100644 packages/inula-reactive/src/EventTypes.ts create mode 100644 packages/inula-reactive/src/dom/DOMExternal.ts create mode 100644 packages/inula-reactive/src/dom/DOMInternalKeys.ts create mode 100644 packages/inula-reactive/src/dom/DOMOperator.ts create mode 100644 packages/inula-reactive/src/dom/DOMPropertiesHandler/DOMPropertiesHandler.ts create mode 100644 packages/inula-reactive/src/dom/DOMPropertiesHandler/StyleHandler.ts create mode 100644 packages/inula-reactive/src/dom/DOMPropertiesHandler/UpdateCommonProp.ts create mode 100644 packages/inula-reactive/src/dom/SelectionRangeHandler.ts create mode 100644 packages/inula-reactive/src/dom/utils/Common.ts create mode 100644 packages/inula-reactive/src/dom/utils/DomCreator.ts create mode 100644 packages/inula-reactive/src/dom/utils/Interface.ts create mode 100644 packages/inula-reactive/src/dom/validators/PropertiesData.ts create mode 100644 packages/inula-reactive/src/dom/validators/ValidateProps.ts create mode 100644 packages/inula-reactive/src/dom/valueHandler/InputValueHandler.ts create mode 100644 packages/inula-reactive/src/dom/valueHandler/OptionValueHandler.ts create mode 100644 packages/inula-reactive/src/dom/valueHandler/SelectValueHandler.ts create mode 100644 packages/inula-reactive/src/dom/valueHandler/TextareaValueHandler.ts create mode 100644 packages/inula-reactive/src/dom/valueHandler/ValueChangeHandler.ts create mode 100644 packages/inula-reactive/src/dom/valueHandler/index.ts create mode 100644 packages/inula-reactive/src/event/EventBinding.ts create mode 100644 packages/inula-reactive/src/event/EventHub.ts create mode 100644 packages/inula-reactive/src/event/EventWrapper.ts create mode 100644 packages/inula-reactive/src/event/FormValueController.ts create mode 100644 packages/inula-reactive/src/event/InulaEventMain.ts create mode 100644 packages/inula-reactive/src/event/ListenerGetter.ts create mode 100644 packages/inula-reactive/src/event/MouseEvent.ts create mode 100644 packages/inula-reactive/src/event/Types.ts create mode 100644 packages/inula-reactive/src/event/utils.ts create mode 100644 packages/inula-reactive/src/external/ChildrenUtil.ts create mode 100644 packages/inula-reactive/src/external/InulaIs.ts create mode 100644 packages/inula-reactive/src/external/JSXElement.ts create mode 100644 packages/inula-reactive/src/external/JSXElementType.ts create mode 100644 packages/inula-reactive/src/external/TestUtil.ts create mode 100644 packages/inula-reactive/src/external/devtools.ts create mode 100644 packages/inula-reactive/src/index.ts create mode 100644 packages/inula-reactive/src/inulax/CommonUtils.ts create mode 100644 packages/inula-reactive/src/inulax/Constants.ts create mode 100644 packages/inula-reactive/src/inulax/adapters/redux.ts create mode 100644 packages/inula-reactive/src/inulax/adapters/reduxReact.ts create mode 100644 packages/inula-reactive/src/inulax/adapters/reduxThunk.ts create mode 100644 packages/inula-reactive/src/inulax/devtools/constants.ts create mode 100644 packages/inula-reactive/src/inulax/devtools/index.ts create mode 100644 packages/inula-reactive/src/inulax/proxy/HooklessObserver.ts create mode 100644 packages/inula-reactive/src/inulax/proxy/Observer.ts create mode 100644 packages/inula-reactive/src/inulax/proxy/ProxyHandler.ts create mode 100644 packages/inula-reactive/src/inulax/proxy/handlers/ArrayProxyHandler.ts create mode 100644 packages/inula-reactive/src/inulax/proxy/handlers/CollectionProxyHandler.ts create mode 100644 packages/inula-reactive/src/inulax/proxy/handlers/MapProxy.ts create mode 100644 packages/inula-reactive/src/inulax/proxy/handlers/ObjectProxyHandler.ts create mode 100644 packages/inula-reactive/src/inulax/proxy/handlers/SetProxy.ts create mode 100644 packages/inula-reactive/src/inulax/proxy/handlers/WeakMapProxy.ts create mode 100644 packages/inula-reactive/src/inulax/proxy/handlers/WeakSetProxy.ts create mode 100644 packages/inula-reactive/src/inulax/proxy/readonlyProxy.ts create mode 100644 packages/inula-reactive/src/inulax/proxy/watch.ts create mode 100644 packages/inula-reactive/src/inulax/store/StoreHandler.ts create mode 100644 packages/inula-reactive/src/inulax/types.ts create mode 100644 packages/inula-reactive/src/jsx-dev-runtime.ts create mode 100644 packages/inula-reactive/src/jsx-runtime.ts create mode 100644 packages/inula-reactive/src/reactive/Atom.ts create mode 100644 packages/inula-reactive/src/reactive/Batch.ts create mode 100644 packages/inula-reactive/src/reactive/Computed.ts create mode 100644 packages/inula-reactive/src/reactive/DiffUtils.ts create mode 100644 packages/inula-reactive/src/reactive/RContext.ts create mode 100644 packages/inula-reactive/src/reactive/RContextCreator.ts create mode 100644 packages/inula-reactive/src/reactive/RNode.ts create mode 100644 packages/inula-reactive/src/reactive/Reactive.ts create mode 100644 packages/inula-reactive/src/reactive/Utils.ts create mode 100644 packages/inula-reactive/src/reactive/Var.ts create mode 100644 packages/inula-reactive/src/reactive/Watch.ts create mode 100644 packages/inula-reactive/src/reactive/components/Block.tsx create mode 100644 packages/inula-reactive/src/reactive/components/For.tsx create mode 100644 packages/inula-reactive/src/reactive/components/RText.ts create mode 100644 packages/inula-reactive/src/reactive/components/Show.tsx create mode 100644 packages/inula-reactive/src/reactive/components/Switch.ts create mode 100644 packages/inula-reactive/src/reactive/proxy/RProxyHandler.ts create mode 100644 packages/inula-reactive/src/reactive/types.ts create mode 100644 packages/inula-reactive/src/renderer/ContextSaver.ts create mode 100644 packages/inula-reactive/src/renderer/ErrorHandler.ts create mode 100644 packages/inula-reactive/src/renderer/ExecuteMode.ts create mode 100644 packages/inula-reactive/src/renderer/GlobalVar.ts create mode 100644 packages/inula-reactive/src/renderer/Renderer.ts create mode 100644 packages/inula-reactive/src/renderer/RootStack.ts create mode 100644 packages/inula-reactive/src/renderer/TreeBuilder.ts create mode 100644 packages/inula-reactive/src/renderer/Types.ts create mode 100644 packages/inula-reactive/src/renderer/UpdateHandler.ts create mode 100644 packages/inula-reactive/src/renderer/components/BaseClassComponent.ts create mode 100644 packages/inula-reactive/src/renderer/components/CreatePortal.ts create mode 100644 packages/inula-reactive/src/renderer/components/CreateRef.ts create mode 100644 packages/inula-reactive/src/renderer/components/ForwardRef.ts create mode 100644 packages/inula-reactive/src/renderer/components/Lazy.ts create mode 100644 packages/inula-reactive/src/renderer/components/Memo.ts create mode 100644 packages/inula-reactive/src/renderer/components/context/Context.ts create mode 100644 packages/inula-reactive/src/renderer/components/context/CreateContext.ts create mode 100644 packages/inula-reactive/src/renderer/diff/DiffTools.ts create mode 100644 packages/inula-reactive/src/renderer/diff/nodeDiffComparator.ts create mode 100644 packages/inula-reactive/src/renderer/hooks/BaseHook.ts create mode 100644 packages/inula-reactive/src/renderer/hooks/EffectConstant.ts create mode 100644 packages/inula-reactive/src/renderer/hooks/HookExternal.ts create mode 100644 packages/inula-reactive/src/renderer/hooks/HookMain.ts create mode 100644 packages/inula-reactive/src/renderer/hooks/HookStage.ts create mode 100644 packages/inula-reactive/src/renderer/hooks/HookType.ts create mode 100644 packages/inula-reactive/src/renderer/hooks/UseCallbackHook.ts create mode 100644 packages/inula-reactive/src/renderer/hooks/UseEffectHook.ts create mode 100644 packages/inula-reactive/src/renderer/hooks/UseImperativeHook.ts create mode 100644 packages/inula-reactive/src/renderer/hooks/UseMemoHook.ts create mode 100644 packages/inula-reactive/src/renderer/hooks/UseReducerHook.ts create mode 100644 packages/inula-reactive/src/renderer/hooks/UseRefHook.ts create mode 100644 packages/inula-reactive/src/renderer/hooks/UseStateHook.ts create mode 100644 packages/inula-reactive/src/renderer/hooks/UseWatch.ts create mode 100644 packages/inula-reactive/src/renderer/hooks/reactive/UseAtom.ts create mode 100644 packages/inula-reactive/src/renderer/hooks/reactive/UseCompute.ts create mode 100644 packages/inula-reactive/src/renderer/hooks/reactive/UseReactive.ts create mode 100644 packages/inula-reactive/src/renderer/hooks/reactive/UseWatch.ts create mode 100644 packages/inula-reactive/src/renderer/render/BaseComponent.ts create mode 100644 packages/inula-reactive/src/renderer/render/ClassComponent.ts create mode 100644 packages/inula-reactive/src/renderer/render/ContextConsumer.ts create mode 100644 packages/inula-reactive/src/renderer/render/ContextProvider.ts create mode 100644 packages/inula-reactive/src/renderer/render/DomComponent.ts create mode 100644 packages/inula-reactive/src/renderer/render/DomPortal.ts create mode 100644 packages/inula-reactive/src/renderer/render/DomText.ts create mode 100644 packages/inula-reactive/src/renderer/render/ForwardRef.ts create mode 100644 packages/inula-reactive/src/renderer/render/Fragment.ts create mode 100644 packages/inula-reactive/src/renderer/render/FunctionComponent.ts create mode 100644 packages/inula-reactive/src/renderer/render/LazyComponent.ts create mode 100644 packages/inula-reactive/src/renderer/render/MemoComponent.ts create mode 100644 packages/inula-reactive/src/renderer/render/ReactiveComponent.ts create mode 100644 packages/inula-reactive/src/renderer/render/SuspenseComponent.ts create mode 100644 packages/inula-reactive/src/renderer/render/TreeRoot.ts create mode 100644 packages/inula-reactive/src/renderer/render/class/ClassLifeCycleProcessor.ts create mode 100644 packages/inula-reactive/src/renderer/render/index.ts create mode 100644 packages/inula-reactive/src/renderer/submit/HookEffectHandler.ts create mode 100644 packages/inula-reactive/src/renderer/submit/LifeCycleHandler.ts create mode 100644 packages/inula-reactive/src/renderer/submit/Submit.ts create mode 100644 packages/inula-reactive/src/renderer/taskExecutor/BrowserAsync.ts create mode 100644 packages/inula-reactive/src/renderer/taskExecutor/RenderQueue.ts create mode 100644 packages/inula-reactive/src/renderer/taskExecutor/TaskExecutor.ts create mode 100644 packages/inula-reactive/src/renderer/taskExecutor/TaskQueue.ts create mode 100644 packages/inula-reactive/src/renderer/utils/compare.ts create mode 100644 packages/inula-reactive/src/renderer/utils/throwIfTrue.ts create mode 100644 packages/inula-reactive/src/renderer/utils/vNodePath.ts create mode 100644 packages/inula-reactive/src/renderer/vnode/VNode.ts create mode 100644 packages/inula-reactive/src/renderer/vnode/VNodeCreator.ts create mode 100644 packages/inula-reactive/src/renderer/vnode/VNodeFlags.ts create mode 100644 packages/inula-reactive/src/renderer/vnode/VNodeShouldUpdate.ts create mode 100644 packages/inula-reactive/src/renderer/vnode/VNodeTags.ts create mode 100644 packages/inula-reactive/src/renderer/vnode/VNodeUtils.ts create mode 100644 packages/inula-reactive/src/temp.js create mode 100644 packages/inula-reactive/src/types.ts create mode 100644 packages/inula-reactive/tsconfig.build.json create mode 100644 packages/inula-reactive/tsconfig.json create mode 100644 packages/inula/scripts/__tests__/temp.test.jsx diff --git a/packages/inula-reactive/.editorconfig b/packages/inula-reactive/.editorconfig new file mode 100644 index 00000000..5a51806d --- /dev/null +++ b/packages/inula-reactive/.editorconfig @@ -0,0 +1,10 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +charset = utf-8 +indent_size = 2 +indent_style = space +trim_trailing_whitespace = true + diff --git a/packages/inula-reactive/.gitignore b/packages/inula-reactive/.gitignore new file mode 100644 index 00000000..f7f1a3cd --- /dev/null +++ b/packages/inula-reactive/.gitignore @@ -0,0 +1,6 @@ +node_modules +.idea +.vscode +package-lock.json +scripts/*.ejs +build diff --git a/packages/inula-reactive/README.md b/packages/inula-reactive/README.md new file mode 100644 index 00000000..2a553e60 --- /dev/null +++ b/packages/inula-reactive/README.md @@ -0,0 +1,174 @@ +# 欢迎使用 InulaJS! + +## 项目介绍: + +InulaJS 是一款用于构建用户界面的Javascript库。 +InulaJS 提供响应式API,相比virtual dom方式,提升渲染效率30%以上。 +InulaJS 提供了5大常用核心组件:状态管理器、路由、国际化、请求组件、应用脚手架,帮助开发者高效、高质量的构筑产品前端。 +InulaJS 同时兼容了React API和相关生态(react-redux、react-router、react-intl、axios等)。 + +## 安装指南 + +欢迎使用响应式前端框架 InulaJS!本指南将为您提供详细的安装步骤,以便您可以开始在前端项目中使用该框架。 + +### 步骤1:安装InulaJS + +您可以通过以下几种方式安装InulaJS + +#### 使用npm安装 + +首先,确保您已经安装了 Node.js。你可以在终端中运行以下命令来检查是否已经安装: + +```shell +node -v +``` + +如果成功显示 Node.js 的版本号,则说明已经安装。 + +在命令行中运行以下命令来通过npm安装 InulaJS: + +```shell +npm install inulaJS +``` + +#### 使用yarn安装 + +首先,确保您已经安装了 Node.js。具体操作可参考使用 npm 安装第一步 + +借来确保您已经安装了 yarn,您可以通过以下命令来安装 Yarn(全局安装): + +```shell +npm install -g yarn +``` + +安装完成后,你可以在终端中运行以下命令来验证 yarn 是否成功安装: + +```shell +yarn --version +``` + +如果成功显示 yarn 的版本号,则说明安装成功。 + +最后,在命令行中运行以下命令来通过yarn安装InulaJS: + +```shell +yarn add inulaJS +``` + +注意:yarn 和 npm 是两个独立的包管理器,您可以根据自己的喜好选择使用哪个。它们可以在同一个项目中共存,但建议在一个项目中只使用其中一个来管理依赖。 + +### 步骤2:开始使用InulaJS + +恭喜!您已经成功安装了InulaJS。现在您可以根据您的项目需求自由使用InulaJS提供的组件和功能。 + +请查阅InulaJS的用户使用指南文档以了解更多关于如何使用和配置框架的详细信息。 + +## 贡献指南 + +本指南会指导你如何为InulaJS贡献自己的一份力量,请你在提出issue或pull request前花费几分钟来了解InulaJS社区的贡献指南。 + +### 行为准则 + +我们有一份**行为准则**,希望所有的贡献者都能遵守,请花时间阅读一遍全文以确保你能明白哪些是可以做的,哪些是不可以做的。 + +#### 我们的承诺 + +身为社区成员、贡献者和领袖,我们承诺使社区参与者不受骚扰,无论其年龄、体型、可见或不可见的缺陷、族裔、性征、性别认同和表达、经验水平、教育程度、社会与经济地位、国籍、相貌、种族、种姓、肤色、宗教信仰、性倾向或性取向如何。 + +我们承诺以有助于建立开放、友善、多样化、包容、健康社区的方式行事和互动。 + +#### 我们的准则 + +**有助于为我们的社区创造积极环境的行为例子包括但不限于:** + +- 表现出对他人的同情和善意 +- 尊重不同的主张、观点和感受 +- 提出和大方接受建设性意见 +- 承担责任并向受我们错误影响的人道歉 +- 注重社区共同诉求,而非个人得失 + +**不当行为例子包括:** + +- 使用情色化的语言或图像,及性引诱或挑逗 +- 嘲弄、侮辱或诋毁性评论,以及人身或政治攻击 +- 公开或私下的骚扰行为 +- 未经他人明确许可,公布他人的私人信息,如物理或电子邮件地址 +- 其他有理由认定为违反职业操守的不当行为 + +#### 责任和权力 + +社区领袖有责任解释和落实我们所认可的行为准则,并妥善公正地对他们认为不当、威胁、冒犯或有害的任何行为采取纠正措施。 + +社区领导有权力和责任删除、编辑或拒绝或拒绝与本行为准则不相符的评论(comment)、提交(commits)、代码、维基(wiki)编辑、议题(issues)或其他贡献,并在适当时机知采取措施的理由。 + +#### 适用范围 + +本行为准则适用于所有社区场合,也适用于在公共场所代表社区时的个人。 + +代表社区的情形包括使用官方电子邮件地址、通过官方社交媒体帐户发帖或在线上或线下活动中担任指定代表。 + +#### 监督 + +辱骂、骚扰或其他不可接受的行为可通过 XX@XXX.com 向负责监督的社区领袖报告。 所有投诉都将得到及时和公平的审查和调查。 + +所有社区领袖都有义务尊重任何事件报告者的隐私和安全。 + +#### 参见 + +本行为准则改编自 Contributor Covenant 2.1 版, 参见 https://www.contributor-covenant.org/version/2/1/code_of_conduct.html。 + +### 公正透明的开发流程 + +我们所有的工作都会放在 [Gitee](https://www.gitee.com) 上。不管是核心团队的成员还是外部贡献者的 pull request 都需要经过同样流程的 review。 + +### 分支管理 + +InulaJS长期维护XX分支。如果你要修复一个Bug或增加一个新的功能,那么请Pull Request到XX分支上 + +### Bug提交 + +我们使用 Gitee Issues来进行Bug跟踪。在你发现Bug后,请通过我们提供的模板来提Issue,以便你发现的Bug能被快速解决。 +在你报告一个 bug 之前,请先确保不和已有Issue重复以及查阅了我们的用户使用指南。 + +### 新增功能 + +如果你有帮助我们改进API或者新增功能的想法,我们同样推荐你使用我们提供Issue模板来新建一个添加新功能的 Issue。 + +### 第一次贡献 + +如果你还不清楚怎么在 Gitee 上提交 Pull Request,你可以通过[这篇文章](https://oschina.gitee.io/opensource-guide/guide/%E7%AC%AC%E4%B8%89%E9%83%A8%E5%88%86%EF%BC%9A%E5%B0%9D%E8%AF%95%E5%8F%82%E4%B8%8E%E5%BC%80%E6%BA%90/%E7%AC%AC%207%20%E5%B0%8F%E8%8A%82%EF%BC%9A%E6%8F%90%E4%BA%A4%E7%AC%AC%E4%B8%80%E4%B8%AA%20Pull%20Request/#%E4%BB%80%E4%B9%88%E6%98%AF-pull-request)学习 + +当你想开始处理一个 issue 时,先检查一下 issue 下面的留言,确保没有其他人正在处理。如果没有,你可以留言告知其他人你将处理这个 issue,避免重复劳动。 + +### 开发指南 + +InulaJS团队会关注所有Pull Request,我们会review以及合入你的代码,也有可能要求你做一些修改或者告诉你我们我们为什么不能接受你的修改。 + +在你发送 Pull Request 之前,请确认你是按照下面的步骤来做的: + +1. 确保基于正确的分支进行修改,详细信息请参考[这里](#分支管理)。 +2. 在项目根目录下运行了 `npm install`。 +3. 如果你修复了一个 bug 或者新增了一个功能,请确保新增或完善了相应的测试,这很重要。 +4. 确认所有的测试都是通过的 `npm run test` +5. 确保你的代码通过了 lint 检查 `npm run lint`. + +#### 常用命令介绍 + +1. `npm run build` 同时构建InulaJS UMD的prod版本和dev版本 +2. `build-types` 单独构建InulaJS的类型提示@types目录 + +#### 配套开发工具 + +- [InulaJS-devtool](https://www.XXXX.com): 可视化InulaJS项目页面的vDom树 + +## 开源许可协议 + +请查阅 License 获取开源许可协议的更多信息. + +版权说明: + +InulaJS 前端框架,版权所有 © 2023-,InulaJS开发团队。保留一切权利。 + +除非另有明示,本网站上的内容采用以下许可证进行许可:Creative Commons Attribution 4.0 International License。 + +如需了解更多信息,请查看完整的许可证协议:https://creativecommons.org/licenses/by/4.0/legalcode diff --git a/packages/inula-reactive/babel.config.js b/packages/inula-reactive/babel.config.js new file mode 100644 index 00000000..ce217777 --- /dev/null +++ b/packages/inula-reactive/babel.config.js @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +module.exports = { + presets: ['@babel/preset-typescript', ['@babel/preset-env', { targets: { node: 'current' } }]], + plugins: [ + '@babel/plugin-syntax-jsx', + [ + '@babel/plugin-transform-react-jsx', + { + pragma: 'Inula.createElement', + pragmaFrag: 'Inula.Fragment', + }, + ], + ['@babel/plugin-proposal-class-properties', { loose: true }], + ['@babel/plugin-proposal-private-methods', { loose: true }], + ['@babel/plugin-proposal-private-property-in-object', { loose: true }], + '@babel/plugin-transform-object-assign', + '@babel/plugin-transform-object-super', + ['@babel/plugin-proposal-object-rest-spread', { loose: true, useBuiltIns: true }], + ['@babel/plugin-transform-template-literals', { loose: true }], + '@babel/plugin-transform-arrow-functions', + '@babel/plugin-transform-literals', + '@babel/plugin-transform-for-of', + '@babel/plugin-transform-block-scoped-functions', + '@babel/plugin-transform-classes', + '@babel/plugin-transform-shorthand-properties', + '@babel/plugin-transform-computed-properties', + '@babel/plugin-transform-parameters', + ['@babel/plugin-transform-spread', { loose: true, useBuiltIns: true }], + ['@babel/plugin-transform-block-scoping', { throwIfClosureRequired: false }], + ['@babel/plugin-transform-destructuring', { loose: true, useBuiltIns: true }], + '@babel/plugin-transform-runtime', + '@babel/plugin-proposal-nullish-coalescing-operator', + '@babel/plugin-proposal-optional-chaining', + ], +}; diff --git a/packages/inula-reactive/global.d.ts b/packages/inula-reactive/global.d.ts new file mode 100644 index 00000000..ebb0bfd0 --- /dev/null +++ b/packages/inula-reactive/global.d.ts @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +/* + 区分是否开发者模式 + */ +declare var isDev: boolean; +declare var isTest: boolean; +declare const __VERSION__: string; +declare var setImmediate: Function; +declare var __INULA_DEV_HOOK__: any; diff --git a/packages/inula-reactive/jest.config.js b/packages/inula-reactive/jest.config.js new file mode 100644 index 00000000..87f24bf7 --- /dev/null +++ b/packages/inula-reactive/jest.config.js @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +module.exports = { + coverageDirectory: 'coverage', + resetModules: true, + + rootDir: process.cwd(), + + setupFiles: [require.resolve('./scripts/__tests__/jest/jestEnvironment.js')], + + setupFilesAfterEnv: [require.resolve('./scripts/__tests__/jest/jestSetting.js')], + + testEnvironment: 'jest-environment-jsdom-sixteen', + + testMatch: [ + // '/scripts/__tests__/InulaXTest/edgeCases/deepVariableObserver.test.tsx', + // '/scripts/__tests__/InulaXTest/StateManager/StateMap.test.tsx', + '/scripts/__tests__/**/*.test.js', + '/scripts/__tests__/**/*.test.tsx', + ], + + timers: 'fake', +}; diff --git a/packages/inula-reactive/npm/index.js b/packages/inula-reactive/npm/index.js new file mode 100644 index 00000000..56a07fcb --- /dev/null +++ b/packages/inula-reactive/npm/index.js @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +'use strict'; + +if (process.env.NODE_ENV === 'production') { + module.exports = require('./cjs/inula.production.min.js'); +} else { + module.exports = require('./cjs/inula.development.js'); +} diff --git a/packages/inula-reactive/package.json b/packages/inula-reactive/package.json new file mode 100644 index 00000000..5700fecb --- /dev/null +++ b/packages/inula-reactive/package.json @@ -0,0 +1,26 @@ +{ + "name": "inulajs-reactive", + "description": "Inulajs is a JavaScript framework library.", + "keywords": [ + "inulajs" + ], + "version": "0.0.11", + "homepage": "", + "bugs": "", + "main": "index.js", + "repository": {}, + "engines": { + "node": ">=0.10.0" + }, + "scripts": { + "build": "rollup --config ./scripts/rollup/rollup.config.js", + "build-types": "tsc -p tsconfig.build.json || echo \"WARNING: TSC exited with status $?\" && rollup -c ./scripts/rollup/build-types.js", + "build:watch": "rollup --watch --config ./scripts/rollup/rollup.config.js", + "debug-test": "yarn test --debug", + "lint": "eslint . --ext .ts --fix", + "prettier": "prettier -w libs/**/*.ts", + "test": "jest --config=jest.config.js", + "watch-test": "yarn test --watch --dev" + }, + "types": "@types/index.d.ts" +} diff --git a/packages/inula-reactive/scripts/__tests__/ActTest/act.test.js b/packages/inula-reactive/scripts/__tests__/ActTest/act.test.js new file mode 100644 index 00000000..6b012c89 --- /dev/null +++ b/packages/inula-reactive/scripts/__tests__/ActTest/act.test.js @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import Inula, { render, useState, act, useEffect } from '../../../src/index'; + +describe('Inula.act function Test', () => { + it('The act can wait for the useEffect update to complete.', function () { + const Parent = props => { + const [buttonOptions, setBtn] = useState([]); + const [checkedRows, setCheckedRows] = useState([]); + + useEffect(() => { + setBtn([1, 2, 3]); + }, [checkedRows.length]); + + return ( +
+ +
+ ); + }; + + const Child = props => { + const { buttonOptions } = props; + const [btnList, setBtnList] = useState(0); + + useEffect(() => { + setBtnList(buttonOptions.length); + }, [buttonOptions]); + + return
{btnList}
; + }; + + act(() => { + render(, container); + }); + + // act能够等待useEffect触发的update完成 + expect(container.querySelector('#childDiv').innerHTML).toEqual('3'); + }); +}); diff --git a/packages/inula-reactive/scripts/__tests__/ComponentTest/ClassRefs.test.js b/packages/inula-reactive/scripts/__tests__/ComponentTest/ClassRefs.test.js new file mode 100644 index 00000000..14c7db23 --- /dev/null +++ b/packages/inula-reactive/scripts/__tests__/ComponentTest/ClassRefs.test.js @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import * as Inula from '../../../src/index'; + +describe('Class refs Test', () => { + it('Parent can get Child instance by refs', function () { + let pInst; + + class Parent extends Inula.Component { + componentDidMount() { + pInst = this; + } + + render() { + return ( +
+ +
childDiv
+
+
+ ); + } + } + + class Child extends Inula.Component { + state = { y: 0 }; + + render() { + return
{this.props.children}
; + } + } + + Inula.render(, container); + + expect(pInst.refs['child'].state.y).toEqual(0); + expect(pInst.refs['childDiv'].innerHTML).toEqual('childDiv'); + }); +}); diff --git a/packages/inula-reactive/scripts/__tests__/ComponentTest/ComponentError.test.js b/packages/inula-reactive/scripts/__tests__/ComponentTest/ComponentError.test.js new file mode 100644 index 00000000..edee5d9c --- /dev/null +++ b/packages/inula-reactive/scripts/__tests__/ComponentTest/ComponentError.test.js @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import * as Inula from '../../../src/index'; +import { getLogUtils } from '../jest/testUtils'; + +describe('Component Error Test', () => { + const LogUtils = getLogUtils(); + it('createElement不能为null或undefined', () => { + const NullElement = null; + const UndefinedElement = undefined; + + jest.spyOn(console, 'error').mockImplementation(); + expect(() => { + Inula.render(, document.createElement('div')); + }).toThrow('Component type is invalid, got: null'); + + expect(() => { + Inula.render(, document.createElement('div')); + }).toThrow('Component type is invalid, got: undefined'); + + const App = () => { + return ; + }; + + let AppChild = () => { + return ; + }; + + expect(() => { + Inula.render(, document.createElement('div')); + }).toThrow('Component type is invalid, got: null'); + + AppChild = () => { + return ; + }; + + expect(() => { + Inula.render(, document.createElement('div')); + }).toThrow('Component type is invalid, got: undefined'); + }); +}); diff --git a/packages/inula-reactive/scripts/__tests__/ComponentTest/Context.test.js b/packages/inula-reactive/scripts/__tests__/ComponentTest/Context.test.js new file mode 100644 index 00000000..f6de0274 --- /dev/null +++ b/packages/inula-reactive/scripts/__tests__/ComponentTest/Context.test.js @@ -0,0 +1,414 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import * as Inula from '../../../src/index'; +import { getLogUtils } from '../jest/testUtils'; + +describe('Context Test', () => { + const LogUtils = getLogUtils(); + it('Provider及其内部consumer组件都不受制于shouldComponentUpdate函数或者Inula.memo()', () => { + const LanguageTypes = { + JAVA: 'Java', + JAVASCRIPT: 'JavaScript', + }; + const defaultValue = { type: LanguageTypes.JAVASCRIPT }; + const SystemLanguageContext = Inula.createContext(defaultValue); + const SystemLanguageConsumer = SystemLanguageContext.Consumer; + const SystemLanguageProvider = props => { + LogUtils.log('SystemLanguageProvider'); + return {props.children}; + }; + + const Consumer = () => { + LogUtils.log('Consumer'); + return ( + + {type => { + LogUtils.log('Consumer DOM mutations'); + return

{type}

; + }} +
+ ); + }; + + class Middle extends Inula.Component { + shouldComponentUpdate() { + return false; + } + render() { + LogUtils.log('Middle'); + return this.props.children; + } + } + + const App = props => { + LogUtils.log('App'); + return ( + + + + + + + + ); + }; + + Inula.render(, container); + expect(container.querySelector('p').innerHTML).toBe('Java'); + expect(LogUtils.getAndClear()).toEqual([ + 'App', + 'SystemLanguageProvider', + 'Middle', + 'Middle', + 'Consumer', + 'Consumer DOM mutations', + ]); + + // 组件不变,Middle没有更新,消费者也不会执行 + Inula.render(, container); + expect(container.querySelector('p').innerHTML).toBe('Java'); + expect(LogUtils.getAndClear()).toEqual(['App', 'SystemLanguageProvider']); + + Inula.render(, container); + expect(container.querySelector('p').innerHTML).toBe('JavaScript'); + // 组件更新,但是Middle没有更新,会绕过Middle + expect(LogUtils.getAndClear()).toEqual(['App', 'SystemLanguageProvider', 'Consumer DOM mutations']); + }); + + it('嵌套consumer provider', () => { + const Num = { + ONE: 1, + TWO: 2, + }; + const NumberContext = Inula.createContext(0); + const NumberConsumer = NumberContext.Consumer; + const NumberProvider = props => { + LogUtils.log(`SystemLanguageProvider: ${props.type}`); + return {props.children}; + }; + + const Consumer = () => { + LogUtils.log('Consumer'); + return ( + + {type => { + LogUtils.log('Consumer DOM mutations'); + return

{type}

; + }} +
+ ); + }; + + class Middle extends Inula.Component { + shouldComponentUpdate() { + return false; + } + render() { + LogUtils.log('Middle'); + return this.props.children; + } + } + + const App = props => { + LogUtils.log('App'); + return ( + + + + + + + + ); + }; + + // Consumer决定于距离它最近的provider + Inula.render(, container); + expect(container.querySelector('p').innerHTML).toBe('2'); + expect(LogUtils.getAndClear()).toEqual([ + 'App', + 'SystemLanguageProvider: 1', + 'SystemLanguageProvider: 2', + 'Middle', + 'Consumer', + 'Consumer DOM mutations', + ]); + // 更新 + Inula.render(, container); + expect(container.querySelector('p').innerHTML).toBe('3'); + expect(LogUtils.getAndClear()).toEqual([ + 'App', + 'SystemLanguageProvider: 2', + 'SystemLanguageProvider: 3', + 'Consumer DOM mutations', + ]); + }); + + it('设置defaultValue', () => { + const Num = { + ONE: 1, + TWO: 2, + }; + const NumberContext = Inula.createContext(0); + const NewNumberContext = Inula.createContext(1); + const NumberConsumer = NumberContext.Consumer; + const NumberProvider = props => { + return {props.children}; + }; + const NewNumberProvider = props => { + return {props.children}; + }; + + class Middle extends Inula.Component { + shouldComponentUpdate() { + return false; + } + render() { + return this.props.children; + } + } + + const NewApp = props => { + return ( + + + + {type => { + LogUtils.log('Consumer DOM mutations'); + return

{type}

; + }} +
+
+
+ ); + }; + + const App = props => { + return ( + + + + {type => { + LogUtils.log('Consumer DOM mutations'); + return

{type}

; + }} +
+
+
+ ); + }; + + Inula.render(, container); + // 没有匹配到Provider,会使用defaultValue + expect(container.querySelector('p').innerHTML).toBe('0'); + + // 更新,设置value为undefined + Inula.render(, container); + // 设置value为undefined时,defaultValue不生效 + expect(container.querySelector('p').innerHTML).toBe(''); + }); + + it('不同provider下的多个consumer', () => { + const NumContext = Inula.createContext(1); + const Consumer = NumContext.Consumer; + + function Provider(props) { + return ( + + {value => {props.children}} + + ); + } + + class Middle extends Inula.Component { + shouldComponentUpdate() { + return false; + } + render() { + return this.props.children; + } + } + + const App = props => { + return ( + + + + + {value =>

{value}

}
+
+
+ + {value =>

{value}

}
+
+
+
+ ); + }; + + Inula.render(, container); + expect(container.querySelector('p').innerHTML).toBe('4'); + expect(container.querySelector('#p').innerHTML).toBe('2'); + + Inula.render(, container); + expect(container.querySelector('p').innerHTML).toBe('6'); + expect(container.querySelector('#p').innerHTML).toBe('3'); + }); + + it('consumer里的child更新是不会重新渲染', () => { + const NumContext = Inula.createContext(1); + const Consumer = NumContext.Consumer; + + let setNum; + const ReturnDom = props => { + const [num, _setNum] = Inula.useState(0); + setNum = _setNum; + LogUtils.log('ReturnDom'); + return

{`Context: ${props.context}, Num: ${num}`}

; + }; + + const App = props => { + return ( + + + {value => { + LogUtils.log('Consumer'); + return ; + }} + + + ); + }; + + Inula.render(, container); + expect(container.querySelector('p').innerHTML).toBe('Context: 2, Num: 0'); + expect(LogUtils.getAndClear()).toEqual(['Consumer', 'ReturnDom']); + setNum(3); + expect(container.querySelector('p').innerHTML).toBe('Context: 2, Num: 3'); + expect(LogUtils.getAndClear()).toEqual(['ReturnDom']); + }); + + it('consumer可以拿到其他context的值', () => { + const NumContext = Inula.createContext(1); + const TypeContext = Inula.createContext('typeA'); + + const NumAndType = () => { + const type = Inula.useContext(TypeContext); + return ( + + {value => { + LogUtils.log('Consumer'); + return

{`Num: ${value}, Type: ${type}`}

; + }} +
+ ); + }; + + const App = props => { + return ( + + + + + + ); + }; + + Inula.render(, container); + expect(container.querySelector('p').innerHTML).toBe('Num: 2, Type: typeB'); + + Inula.render(, container); + expect(container.querySelector('p').innerHTML).toBe('Num: 2, Type: typeR'); + + Inula.render(, container); + expect(container.querySelector('p').innerHTML).toBe('Num: 8, Type: typeR'); + }); + + // antd menu 级连context场景,menu路径使用级联context实现 + it('nested context', () => { + const NestedContext = Inula.createContext([]); + let updateContext; + + function App() { + const [state, useState] = Inula.useState([]); + updateContext = useState; + return ( + + + + + ); + } + + const div1Ref = Inula.createRef(); + const div2Ref = Inula.createRef(); + + let updateSub1; + function Sub1() { + const path = Inula.useContext(NestedContext); + const [_, setState] = Inula.useState({}); + updateSub1 = () => setState({}); + return ( + + + + ); + } + + function Sub2() { + const path = Inula.useContext(NestedContext); + + return ( + + + + ); + } + + function Sub3() { + const path = Inula.useContext(NestedContext); + + return ( + + + + ); + } + + function Son({ divRef }) { + const path = Inula.useContext(NestedContext); + return ( + +
{path.join(',')}
+
+ ); + } + + Inula.render(, container); + updateSub1(); + expect(div1Ref.current.innerHTML).toEqual('1'); + expect(div2Ref.current.innerHTML).toEqual('2,3'); + + updateContext([0]); + expect(div1Ref.current.innerHTML).toEqual('0,1'); + expect(div2Ref.current.innerHTML).toEqual('0,2,3'); + + // 局部更新Sub1 + updateSub1(); + expect(div1Ref.current.innerHTML).toEqual('0,1'); + expect(div2Ref.current.innerHTML).toEqual('0,2,3'); + }); +}); diff --git a/packages/inula-reactive/scripts/__tests__/ComponentTest/DiffAlgorithm.test.js b/packages/inula-reactive/scripts/__tests__/ComponentTest/DiffAlgorithm.test.js new file mode 100644 index 00000000..54e102dc --- /dev/null +++ b/packages/inula-reactive/scripts/__tests__/ComponentTest/DiffAlgorithm.test.js @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import * as Inula from '../../../src/index'; + +describe('Diff Algorithm', () => { + it('null should diff correctly', () => { + const fn = jest.fn(); + + class C extends Inula.Component { + constructor() { + super(); + fn(); + } + + render() { + return 1; + } + } + + let update; + + function App() { + const [current, setCurrent] = Inula.useState(1); + update = setCurrent; + return ( + <> + {current === 1 ? : null} + {current === 2 ? : null} + {current === 3 ? : null} + + ); + } + + Inula.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/packages/inula-reactive/scripts/__tests__/ComponentTest/ForwardRef.test.js b/packages/inula-reactive/scripts/__tests__/ComponentTest/ForwardRef.test.js new file mode 100644 index 00000000..2e861595 --- /dev/null +++ b/packages/inula-reactive/scripts/__tests__/ComponentTest/ForwardRef.test.js @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import * as Inula from '../../../src/index'; +import { getLogUtils } from '../jest/testUtils'; + +describe('ForwardRef', () => { + const LogUtils = getLogUtils(); + it('ForwardRef包裹的函数组件应该正常触发effect', () => { + function App(props, ref) { + Inula.useEffect(() => { + LogUtils.log('effect'); + return () => { + LogUtils.log('effect remove'); + }; + }); + return ; + } + + const Wrapper = Inula.forwardRef(App); + + Inula.act(() => { + Inula.render(, container); + }); + expect(LogUtils.getAndClear()).toEqual(['effect']); + Inula.act(() => { + Inula.render(, container); + }); + expect(LogUtils.getAndClear()).toEqual(['effect remove', 'effect']); + }); + + it('memo组件包裹的类组件', () => { + class Component extends Inula.Component { + render() { + return ; + } + } + + const Wrapper = Inula.memo(Component); + + Inula.act(() => { + Inula.render(, container); + }); + Inula.act(() => { + Inula.render(, container); + }); + }); +}); diff --git a/packages/inula-reactive/scripts/__tests__/ComponentTest/FragmentComponent.test.js b/packages/inula-reactive/scripts/__tests__/ComponentTest/FragmentComponent.test.js new file mode 100644 index 00000000..c91c5dac --- /dev/null +++ b/packages/inula-reactive/scripts/__tests__/ComponentTest/FragmentComponent.test.js @@ -0,0 +1,476 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import * as Inula from '../../../src/index'; +import { Text } from '../jest/commonComponents'; +import { getLogUtils } from '../jest/testUtils'; + +describe('Fragment', () => { + const LogUtils = getLogUtils(); + const { useEffect, useRef, act } = Inula; + it('可以渲染空元素', () => { + const element = ; + + Inula.render(element, container); + + expect(container.textContent).toBe(''); + }); + it('可以渲染单个元素', () => { + const element = ( + + + + ); + + Inula.render(element, container); + + expect(LogUtils.getAndClear()).toEqual(['Fragment']); + expect(container.textContent).toBe('Fragment'); + }); + + it('可以渲染混合元素', () => { + const element = ( + + Java and + + ); + + Inula.render(element, container); + + expect(LogUtils.getAndClear()).toEqual(['JavaScript']); + expect(container.textContent).toBe('Java and JavaScript'); + }); + + it('可以渲染集合元素', () => { + const App = [, ]; + const element = <>{App}; + + Inula.render(element, container); + + expect(LogUtils.getAndClear()).toEqual(['Java', 'JavaScript']); + expect(container.textContent).toBe('JavaJavaScript'); + }); + + it('元素被放进不同层级Fragment里时,状态不会保留', () => { + const ChildApp = props => { + const flag = useRef(true); + useEffect(() => { + if (flag.current) { + flag.current = false; + } else { + LogUtils.log('useEffect'); + } + }); + + return

{props.logo}

; + }; + + const App = props => { + return props.change ? ( + <> + + + ) : ( + <> + <> + + + + ); + }; + + act(() => { + Inula.render(, container); + }); + expect(LogUtils.getNotClear()).toEqual([]); + act(() => { + Inula.render(, container); + }); + // 切换到不同层级Fragment时,副作用状态不会保留 + expect(LogUtils.getNotClear()).toEqual([]); + expect(container.textContent).toBe('2'); + + act(() => { + Inula.render(, container); + }); + expect(LogUtils.getNotClear()).toEqual([]); + expect(container.textContent).toBe('1'); + }); + + it('元素被放进单层Fragment里,且在Fragment的顶部时,状态会保留', () => { + const ChildApp = props => { + const flag = useRef(true); + useEffect(() => { + if (flag.current) { + flag.current = false; + } else { + LogUtils.log('useEffect'); + } + }); + + return

{props.logo}

; + }; + + const App = props => { + return props.change ? ( + + ) : ( + <> + + + ); + }; + + act(() => { + Inula.render(, container); + }); + expect(LogUtils.getNotClear()).toEqual([]); + act(() => { + Inula.render(, container); + }); + // 状态会保留 + expect(LogUtils.getNotClear()).toEqual(['useEffect']); + expect(container.textContent).toBe('2'); + + act(() => { + Inula.render(, container); + }); + expect(LogUtils.getNotClear()).toEqual(['useEffect', 'useEffect']); + expect(container.textContent).toBe('1'); + }); + + it('元素被放进单层Fragment里,但不在Fragment的顶部时,状态不会保留', () => { + const ChildApp = props => { + const flag = useRef(true); + useEffect(() => { + if (flag.current) { + flag.current = false; + } else { + LogUtils.log('useEffect'); + } + }); + + return

{props.logo}

; + }; + + const App = props => { + return props.change ? ( + + ) : ( + <> +
123
+ + + ); + }; + + act(() => { + Inula.render(, container); + }); + expect(LogUtils.getNotClear()).toEqual([]); + act(() => { + Inula.render(, container); + }); + // 状态不会保留 + expect(LogUtils.getNotClear()).toEqual([]); + expect(container.textContent).toBe('1232'); + + act(() => { + Inula.render(, container); + }); + expect(LogUtils.getNotClear()).toEqual([]); + expect(container.textContent).toBe('1'); + }); + + it('元素被放进多层Fragment里时,状态不会保留', () => { + const ChildApp = props => { + const flag = useRef(true); + useEffect(() => { + if (flag.current) { + flag.current = false; + } else { + LogUtils.log('useEffect'); + } + }); + + return

{props.logo}

; + }; + + const App = props => { + return props.change ? ( + + ) : ( + <> + <> + <> + + + + + ); + }; + + act(() => { + Inula.render(, container); + }); + expect(LogUtils.getNotClear()).toEqual([]); + act(() => { + Inula.render(, container); + }); + // 状态不会保留 + expect(LogUtils.getNotClear()).toEqual([]); + expect(container.textContent).toBe('2'); + + act(() => { + Inula.render(, container); + }); + expect(LogUtils.getNotClear()).toEqual([]); + expect(container.textContent).toBe('1'); + }); + + it('元素被切换放进同级Fragment里时,状态会保留', () => { + const ChildApp = props => { + const flag = useRef(true); + useEffect(() => { + if (flag.current) { + flag.current = false; + } else { + LogUtils.log('useEffect'); + } + }); + + return

{props.logo}

; + }; + + const App = props => { + return props.change ? ( + <> + <> + <> + + + + + ) : ( + <> + <> + <> + + + + + ); + }; + + act(() => { + Inula.render(, container); + }); + expect(LogUtils.getNotClear()).toEqual([]); + act(() => { + Inula.render(, container); + }); + // 状态会保留 + expect(LogUtils.getNotClear()).toEqual(['useEffect']); + expect(container.textContent).toBe('2'); + + act(() => { + Inula.render(, container); + }); + expect(LogUtils.getNotClear()).toEqual(['useEffect', 'useEffect']); + expect(container.textContent).toBe('1'); + }); + + it('元素被切换放进同级Fragment,且在数组顶层时,状态会保留', () => { + const ChildApp = props => { + const flag = useRef(true); + useEffect(() => { + if (flag.current) { + flag.current = false; + } else { + LogUtils.log('useEffect'); + } + }); + + return

{props.logo}

; + }; + + const App = props => { + return props.change ? ( + <> + <> + <> + + + + + ) : ( + <> + <> + <>{[]} + + + ); + }; + + act(() => { + Inula.render(, container); + }); + expect(LogUtils.getNotClear()).toEqual([]); + act(() => { + Inula.render(, container); + }); + // 状态会保留 + expect(LogUtils.getNotClear()).toEqual(['useEffect']); + expect(container.textContent).toBe('2'); + + act(() => { + Inula.render(, container); + }); + expect(LogUtils.getNotClear()).toEqual(['useEffect', 'useEffect']); + expect(container.textContent).toBe('1'); + }); + + it('数组里的顶层元素被切换放进单级Fragment时,状态会保留', () => { + const ChildApp = props => { + const flag = useRef(true); + useEffect(() => { + if (flag.current) { + flag.current = false; + } else { + LogUtils.log('useEffect'); + } + }); + + return

{props.logo}

; + }; + + const App = props => { + return props.change ? ( + [] + ) : ( + <> + + + ); + }; + + act(() => { + Inula.render(, container); + }); + expect(LogUtils.getNotClear()).toEqual([]); + act(() => { + Inula.render(, container); + }); + // 状态会保留 + expect(LogUtils.getNotClear()).toEqual(['useEffect']); + expect(container.textContent).toBe('2'); + + act(() => { + Inula.render(, container); + }); + expect(LogUtils.getNotClear()).toEqual(['useEffect', 'useEffect']); + expect(container.textContent).toBe('1'); + }); + + it('Fragment里的顶层数组里的顶层元素被切换放进不同级Fragment时,状态不会保留', () => { + const ChildApp = props => { + const flag = useRef(true); + useEffect(() => { + if (flag.current) { + flag.current = false; + } else { + LogUtils.log('useEffect'); + } + }); + + return

{props.logo}

; + }; + + const App = props => { + return props.change ? ( + <> + [] + + ) : ( + <> + <> + + + + ); + }; + + act(() => { + Inula.render(, container); + }); + expect(LogUtils.getNotClear()).toEqual([]); + act(() => { + Inula.render(, container); + }); + // 状态会保留 + expect(LogUtils.getNotClear()).toEqual([]); + expect(container.textContent).toBe('2'); + + act(() => { + Inula.render(, container); + }); + expect(LogUtils.getNotClear()).toEqual([]); + expect(container.textContent).toBe('[1]'); + }); + + it('Fragment的key值不同时,状态不会保留', () => { + const ChildApp = props => { + const flag = useRef(true); + useEffect(() => { + if (flag.current) { + flag.current = false; + } else { + LogUtils.log('useEffect'); + } + }); + + return

{props.logo}

; + }; + + const App = props => { + return props.change ? ( + + + + ) : ( + + + + ); + }; + + act(() => { + Inula.render(, container); + }); + expect(LogUtils.getNotClear()).toEqual([]); + act(() => { + Inula.render(, container); + }); + // 状态不会保留 + expect(LogUtils.getNotClear()).toEqual([]); + expect(container.textContent).toBe('2'); + + act(() => { + Inula.render(, container); + }); + expect(LogUtils.getNotClear()).toEqual([]); + expect(container.textContent).toBe('1'); + }); +}); diff --git a/packages/inula-reactive/scripts/__tests__/ComponentTest/FunctionComponent.test.js b/packages/inula-reactive/scripts/__tests__/ComponentTest/FunctionComponent.test.js new file mode 100644 index 00000000..0b5d18a8 --- /dev/null +++ b/packages/inula-reactive/scripts/__tests__/ComponentTest/FunctionComponent.test.js @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import * as Inula from '../../../src/index'; +describe('FunctionComponent Test', () => { + it('渲染无状态组件', () => { + const App = props => { + return

{props.text}

; + }; + + Inula.render(, container); + expect(container.querySelector('p').innerHTML).toBe('app'); + }); + + it('更新无状态组件', () => { + const App = props => { + return

{props.text}

; + }; + + Inula.render(, container); + expect(container.querySelector('p').innerHTML).toBe('app'); + + Inula.render(, container); + expect(container.querySelector('p').innerHTML).toBe('ABC'); + + Inula.render(, container); + expect(container.querySelector('p').innerHTML).toBe('abc'); + }); + + it('卸载无状态组件', () => { + const App = props => { + return

{props.text}

; + }; + + Inula.render(, container); + expect(container.querySelector('p').innerHTML).toBe('app'); + + Inula.unmountComponentAtNode(container); + expect(container.querySelector('p')).toBe(null); + }); + + it('渲染空组件返回空子节点', () => { + const App = () => { + return
; + }; + + const realNode = Inula.render(, container); + expect(realNode).toBe(null); + }); + + it('测试函数组件的defaultProps:Inula.memo(Inula.forwardRef(()=>{}))两层包装的场景后,defaultProps依然正常', () => { + const App = () => { + return ; + }; + + const DefaultPropsComp = Inula.forwardRef(props => { + return
{props.name}
; + }); + DefaultPropsComp.defaultProps = { + name: 'Hello!', + }; + const DefaultPropsCompMemo = Inula.memo(DefaultPropsComp); + + Inula.render(, container); + expect(container.querySelector('div').innerHTML).toBe('Hello!'); + }); + + it('测试', () => { + const App = () => { + return ; + }; + + const StyleComp = props => { + return
{props.name}
; + }; + + Inula.render(, container); + expect(container.querySelector('div').style['_values']['--max-segment-num']).toBe(10); + }); +}); diff --git a/packages/inula-reactive/scripts/__tests__/ComponentTest/HookTest/UseCallback.test.js b/packages/inula-reactive/scripts/__tests__/ComponentTest/HookTest/UseCallback.test.js new file mode 100644 index 00000000..5dc7a697 --- /dev/null +++ b/packages/inula-reactive/scripts/__tests__/ComponentTest/HookTest/UseCallback.test.js @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import * as Inula from '../../../../src/index'; + +describe('useCallback Hook Test', () => { + const { useState, useCallback } = Inula; + + it('测试useCallback', () => { + const App = props => { + const [num, setNum] = useState(0); + const NumUseCallback = useCallback(() => { + setNum(num + props.text); + }, [props]); + return ( + <> +

{num}

+ +
{data}
+
+ ); + }; + + Inula.render(
, container); + Inula.act(() => { + btnRef.current.click(); + }); + expect(nextId).toBe(2); + }); +}); diff --git a/packages/inula-reactive/scripts/__tests__/ComponentTest/HookTest/UseRef.test.js b/packages/inula-reactive/scripts/__tests__/ComponentTest/HookTest/UseRef.test.js new file mode 100644 index 00000000..08c6ad69 --- /dev/null +++ b/packages/inula-reactive/scripts/__tests__/ComponentTest/HookTest/UseRef.test.js @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import * as Inula from '../../../../src/index'; +import { getLogUtils } from '../../jest/testUtils'; +import { Text } from '../../jest/commonComponents'; + +describe('useRef Hook Test', () => { + const { useState, useRef } = Inula; + const LogUtils = getLogUtils(); + + it('测试useRef', () => { + const App = () => { + const [num, setNum] = useState(1); + const ref = useRef(); + if (!ref.current) { + ref.current = num; + } + return ( + <> +

{num}

+

{ref.current}

+ + ; + + ); + }; + Inula.render(, container); + expect(LogUtils.getAndClear()).toEqual([1]); + expect(container.querySelector('p').innerHTML).toBe('1'); + // 点击按钮触发ref.current加1 + container.querySelector('button').click(); + // ref.current改变不会触发重新渲染 + expect(LogUtils.getAndClear()).toEqual([]); + expect(container.querySelector('p').innerHTML).toBe('1'); + }); +}); diff --git a/packages/inula-reactive/scripts/__tests__/ComponentTest/HookTest/UseState.test.js b/packages/inula-reactive/scripts/__tests__/ComponentTest/HookTest/UseState.test.js new file mode 100644 index 00000000..2602b711 --- /dev/null +++ b/packages/inula-reactive/scripts/__tests__/ComponentTest/HookTest/UseState.test.js @@ -0,0 +1,178 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import * as Inula from '../../../../src/index'; +import { getLogUtils } from '../../jest/testUtils'; +import { Text } from '../../jest/commonComponents'; + +describe('useState Hook Test', () => { + const { useState, forwardRef, useImperativeHandle, memo, act } = Inula; + const LogUtils = getLogUtils(); + + it('简单使用useState', () => { + const App = () => { + const [num, setNum] = useState(0); + return ( + <> +

{num}

+ + + ); + }; + + const App = () => { + const handleClick = () => { + LogUtils.log('bubble click event'); + }; + + const handleCaptureClick = () => { + LogUtils.log('capture click event'); + }; + + return ( +
+ }> +
+ ); + }; + Inula.render(, container); + const event = document.createEvent('Event'); + event.initEvent('click', true, true); + buttonRef.current.dispatchEvent(event); + + expect(LogUtils.getAndClear()).toEqual([ + // 从外到内先捕获再冒泡 + 'capture click event', + 'bubble click event', + ]); + }); + + it('Create portal at app root should not add event listener multiple times', () => { + const btnRef = Inula.createRef(); + + class PortalApp extends Inula.Component { + constructor(props) { + super(props); + } + + render() { + return Inula.createPortal(this.props.child, container); + } + } + + const onClick = jest.fn(); + + class App extends Inula.Component { + constructor(props) { + super(props); + } + + render() { + return ( +
+ + +
+ ); + } + } + + Inula.render(, container); + btnRef.current.click(); + expect(onClick).toHaveBeenCalledTimes(1); + }); + + it('#76 Portal onChange should activate', () => { + class Dialog extends Inula.Component { + node; + + constructor(props) { + super(props); + this.node = window.document.createElement('div'); + window.document.body.appendChild(this.node); + } + + render() { + return Inula.createPortal(this.props.children, this.node); + } + } + + let showPortalInput; + const fn = jest.fn(); + const inputRef = Inula.createRef(); + + function App() { + const Input = () => { + const [show, setShow] = Inula.useState(false); + showPortalInput = setShow; + + Inula.useEffect(() => { + setTimeout(() => { + setShow(true); + }, 0); + }, []); + + if (!show) { + return null; + } + + return ; + }; + + return ( +
+ + + +
+ ); + } + + Inula.render(, container); + showPortalInput(true); + jest.advanceTimersToNextTimer(); + dispatchChangeEvent(inputRef.current, 'test'); + expect(fn).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/inula-reactive/scripts/__tests__/ComponentTest/SuspenseComponent.test.js b/packages/inula-reactive/scripts/__tests__/ComponentTest/SuspenseComponent.test.js new file mode 100644 index 00000000..6c1469d3 --- /dev/null +++ b/packages/inula-reactive/scripts/__tests__/ComponentTest/SuspenseComponent.test.js @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import * as Inula from '../../../src/index'; +import { Text } from '../jest/commonComponents'; +import { getLogUtils } from '../jest/testUtils'; + +describe('SuspenseComponent Test', () => { + const LogUtils = getLogUtils(); + const mockImport = jest.fn(async component => { + return { default: component }; + }); + + // var EMPTY_OBJECT = {}; + // const mockCreateResource = jest.fn((component) => { + // let result = EMPTY_OBJECT; + // return () =>{ + // component().then(res => { + // LogUtils.log(res); + // result = res; + // }, reason => { + // LogUtils.log(reason); + // }); + // if(result === EMPTY_OBJECT){ + // throw component(); + // } + // return result; + // }; + // }); + + it('挂载lazy组件', async () => { + // 用同步的代码来实现异步操作 + class LazyComponent extends Inula.Component { + render() { + return ; + } + } + + const Lazy = Inula.lazy(() => mockImport(LazyComponent)); + + Inula.render( + }> + + , + container + ); + + expect(LogUtils.getAndClear()).toEqual(['Loading...']); + expect(container.textContent).toBe('Loading...'); + + await Promise.resolve(); + Inula.render( + }> + + , + container + ); + expect(LogUtils.getAndClear()).toEqual([5]); + expect(container.querySelector('p').innerHTML).toBe('5'); + }); +}); diff --git a/packages/inula-reactive/scripts/__tests__/DomTest/Attribute.test.js b/packages/inula-reactive/scripts/__tests__/DomTest/Attribute.test.js new file mode 100644 index 00000000..9c9b418a --- /dev/null +++ b/packages/inula-reactive/scripts/__tests__/DomTest/Attribute.test.js @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import * as Inula from '../../../src/index'; + +describe('Dom Attribute', () => { + it('属性值为null或undefined时,不会设置此属性', () => { + Inula.render(
, container); + expect(container.querySelector('div').getAttribute('id')).toBe('div'); + Inula.render(
, container); + expect(container.querySelector('div').hasAttribute('id')).toBe(false); + Inula.render(
, container); + expect(container.querySelector('div').hasAttribute('id')).toBe(false); + }); + + it('可以设置未知的属性', () => { + Inula.render(
, container); + expect(container.querySelector('div').hasAttribute('abcd')).toBe(true); + expect(container.querySelector('div').getAttribute('abcd')).toBe('abcd'); + }); + + it('未知属性的值为null或undefined时,不会设置此属性', () => { + Inula.render(
, container); + expect(container.querySelector('div').hasAttribute('abcd')).toBe(false); + Inula.render(
, container); + expect(container.querySelector('div').hasAttribute('abcd')).toBe(false); + }); + + it('未知属性的值为数字时,属性值会转为字符串', () => { + Inula.render(
, container); + expect(container.querySelector('div').getAttribute('abcd')).toBe('0'); + Inula.render(
, container); + expect(container.querySelector('div').getAttribute('abcd')).toBe('-3'); + Inula.render(
, container); + expect(container.querySelector('div').getAttribute('abcd')).toBe('123.45'); + }); + + it('访问节点的标准属性时可以拿到属性值,访问节点的非标准属性时会得到undefined', () => { + Inula.render(
, container); + expect(container.querySelector('div').id).toBe('div'); + expect(container.querySelector('div').abcd).toBe(undefined); + }); + + it('特性方法', () => { + Inula.render(
, container); + expect(container.querySelector('div').hasAttribute('abcd')).toBe(true); + expect(container.querySelector('div').getAttribute('abcd')).toBe('0'); + container.querySelector('div').setAttribute('abcd', 4); + expect(container.querySelector('div').getAttribute('abcd')).toBe('4'); + container.querySelector('div').removeAttribute('abcd'); + expect(container.querySelector('div').hasAttribute('abcd')).toBe(false); + }); + + it('特性大小写不敏感', () => { + Inula.render(
, container); + expect(container.querySelector('div').hasAttribute('abcd')).toBe(true); + expect(container.querySelector('div').hasAttribute('ABCD')).toBe(true); + expect(container.querySelector('div').getAttribute('abcd')).toBe('0'); + expect(container.querySelector('div').getAttribute('ABCD')).toBe('0'); + }); + + it('使用 data- 开头的特性时,会映射到DOM的dataset属性且中划线格式会变成驼峰格式', () => { + Inula.render(
, container); + container.querySelector('div').setAttribute('data-first-name', 'Tom'); + expect(container.querySelector('div').dataset.firstName).toBe('Tom'); + }); + + it('style 自动加px', () => { + const div = Inula.render(
, container); + expect(window.getComputedStyle(div).getPropertyValue('width')).toBe('10px'); + expect(window.getComputedStyle(div).getPropertyValue('height')).toBe('20px'); + }); + + it('WebkitLineClamp和lineClamp样式不会把数字转换成字符串或者追加"px"', () => { + Inula.render(
, container); + // 浏览器可以将WebkitLineClamp识别为-webkit-line-clamp,测试框架不可以 + expect(container.querySelector('div').style.WebkitLineClamp).toBe(2); + }); + + it('空字符串做属性名', () => { + const emptyStringProps = { '': '' }; + expect(() => { + Inula.render(
, container); + }).not.toThrow(); + }); +}); diff --git a/packages/inula-reactive/scripts/__tests__/DomTest/DomInput.test.js b/packages/inula-reactive/scripts/__tests__/DomTest/DomInput.test.js new file mode 100644 index 00000000..2f237b16 --- /dev/null +++ b/packages/inula-reactive/scripts/__tests__/DomTest/DomInput.test.js @@ -0,0 +1,362 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +/* eslint-disable @typescript-eslint/no-empty-function */ +import * as Inula from '../../../src/index'; +import { getLogUtils } from '../jest/testUtils'; + +describe('Dom Input', () => { + const { act } = Inula; + const LogUtils = getLogUtils(); + + describe('type checkbox', () => { + it('没有设置checked属性时,控制台不会报错', () => { + expect(() => Inula.render(, container)).not.toThrow(); + }); + + it('checked属性为undefined或null时且没有onChange属性或没有readOnly={true},控制台不会报错', () => { + expect(() => Inula.render(, container)).not.toThrow(); + expect(() => Inula.render(, container)).not.toThrow(); + }); + + it('复选框的value属性值可以改变', () => { + Inula.render( + { + LogUtils.log('checkbox click'); + }} + />, + container + ); + Inula.render( + { + LogUtils.log('checkbox click'); + }} + />, + container + ); + expect(LogUtils.getAndClear()).toEqual([]); + expect(container.querySelector('input').value).toBe('0'); + expect(container.querySelector('input').getAttribute('value')).toBe('0'); + }); + + it('复选框不设置value属性值时会设置value为"on"', () => { + Inula.render(, container); + expect(container.querySelector('input').value).toBe('on'); + expect(container.querySelector('input').hasAttribute('value')).toBe(false); + }); + + it('测试defaultChecked与更改defaultChecked', () => { + Inula.render(, container); + expect(container.querySelector('input').value).toBe('on'); + expect(container.querySelector('input').checked).toBe(false); + + Inula.render(, container); + expect(container.querySelector('input').value).toBe('on'); + expect(container.querySelector('input').checked).toBe(true); + + Inula.render(, container); + expect(container.querySelector('input').value).toBe('on'); + expect(container.querySelector('input').checked).toBe(true); + }); + }); + + describe('type text', () => { + it('value属性为undefined或null时且没有onChange属性或没有readOnly={true},控制台不会报错', () => { + expect(() => Inula.render(, container)).not.toThrow(); + expect(() => Inula.render(, container)).not.toThrow(); + expect(() => Inula.render(, container)).not.toThrow(); + }); + + it('value值会转为字符串', () => { + const realNode = Inula.render(, container); + expect(realNode.value).toBe('1'); + }); + + it('value值可以被设置为true/false', () => { + let realNode = Inula.render(, container); + expect(realNode.value).toBe('1'); + realNode = Inula.render(, container); + expect(realNode.value).toBe('true'); + realNode = Inula.render(, container); + expect(realNode.value).toBe('false'); + }); + + it('value值可以被设置为object', () => { + let realNode = Inula.render(, container); + expect(realNode.value).toBe('1'); + const value = { + toString: () => { + return 'value'; + }, + }; + realNode = Inula.render(, container); + expect(realNode.value).toBe('value'); + }); + + it('设置defaultValue', () => { + let realNode = Inula.render(, container); + expect(realNode.value).toBe('1'); + expect(realNode.getAttribute('value')).toBe('1'); + Inula.unmountComponentAtNode(container); + // 测试defaultValue为boolean类型 + realNode = Inula.render(, container); + expect(realNode.value).toBe('true'); + expect(realNode.getAttribute('value')).toBe('true'); + + Inula.unmountComponentAtNode(container); + realNode = Inula.render(, container); + expect(realNode.value).toBe('false'); + expect(realNode.getAttribute('value')).toBe('false'); + + Inula.unmountComponentAtNode(container); + const value = { + toString: () => { + return 'default'; + }, + }; + realNode = Inula.render(, container); + expect(realNode.value).toBe('default'); + expect(realNode.getAttribute('value')).toBe('default'); + }); + + it('value为0、defaultValue为1,input 的value应该为0', () => { + const input = Inula.render(, container); + expect(input.getAttribute('value')).toBe('0'); + }); + + it('name属性', () => { + let realNode = Inula.render(, container); + expect(realNode.name).toBe('name'); + expect(realNode.getAttribute('name')).toBe('name'); + Inula.unmountComponentAtNode(container); + // 没有设置name属性 + realNode = Inula.render(, container); + expect(realNode.name).toBe(''); + expect(realNode.getAttribute('name')).toBe(null); + }); + + it('受控input可以触发onChange', () => { + let realNode = Inula.render( + , + container + ); + Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set.call(realNode, 'abcd'); + // 再触发事件 + realNode.dispatchEvent( + new Event('input', { + bubbles: true, + cancelable: true, + }) + ); + // 触发onChange + expect(LogUtils.getAndClear()).toEqual(['text change']); + }); + }); + + describe('type radio', () => { + it('radio的value可以更新', () => { + let realNode = Inula.render(, container); + expect(realNode.value).toBe(''); + expect(realNode.getAttribute('value')).toBe(''); + realNode = Inula.render(, container); + expect(realNode.value).toBe('false'); + expect(realNode.getAttribute('value')).toBe('false'); + realNode = Inula.render(, container); + expect(realNode.value).toBe('true'); + expect(realNode.getAttribute('value')).toBe('true'); + }); + + it('相同name且在同一表单的radio互斥', () => { + Inula.render( + <> + + + +
+ {}} defaultChecked={true} /> +
+ , + container + ); + expect(container.querySelector('input').checked).toBe(true); + expect(document.getElementById('b').checked).toBe(false); + expect(document.getElementById('c').checked).toBe(false); + expect(document.getElementById('d').checked).toBe(true); + // 模拟点击id为b的单选框,b为选中状态,ac为非选中状态 + Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'checked').set.call( + document.getElementById('b'), + true + ); + expect(LogUtils.getAndClear()).toEqual(['a change', 'b change', 'c change']); + expect(container.querySelector('input').checked).toBe(false); + expect(document.getElementById('b').checked).toBe(true); + expect(document.getElementById('c').checked).toBe(false); + expect(document.getElementById('d').checked).toBe(true); + }); + + it('name改变不影响相同name的radio', () => { + const inputRef = Inula.createRef(); + const App = () => { + const [isNum, setNum] = Inula.useState(false); + const inputName = isNum ? 'secondName' : 'firstName'; + + const buttonClick = () => { + setNum(true); + }; + + return ( +
+
+ ); + }; + Inula.render(, container); + expect(container.querySelector('input').checked).toBe(false); + expect(inputRef.current.checked).toBe(true); + // 点击button,触发setNum + container.querySelector('button').click(); + expect(container.querySelector('input').checked).toBe(true); + expect(inputRef.current.checked).toBe(false); + }); + }); + + describe('type submit', () => { + it('type submit value', () => { + Inula.render(, container); + expect(container.querySelector('input').hasAttribute('value')).toBe(false); + Inula.unmountComponentAtNode(container); + + Inula.render(, container); + expect(container.querySelector('input').hasAttribute('value')).toBe(true); + expect(container.querySelector('input').getAttribute('value')).toBe(''); + + Inula.render(, container); + expect(container.querySelector('input').hasAttribute('value')).toBe(true); + expect(container.querySelector('input').getAttribute('value')).toBe('submit'); + }); + }); + + describe('type reset', () => { + it('type reset value', () => { + Inula.render(, container); + expect(container.querySelector('input').hasAttribute('value')).toBe(false); + Inula.unmountComponentAtNode(container); + + Inula.render(, container); + expect(container.querySelector('input').hasAttribute('value')).toBe(true); + expect(container.querySelector('input').getAttribute('value')).toBe(''); + + Inula.render(, container); + expect(container.querySelector('input').hasAttribute('value')).toBe(true); + expect(container.querySelector('input').getAttribute('value')).toBe('reset'); + }); + }); + + describe('type number', () => { + it('value值会把number类型转为字符串,且.xx转为0.xx', () => { + Inula.render(, container); + expect(container.querySelector('input').hasAttribute('value')).toBe(true); + expect(container.querySelector('input').getAttribute('value')).toBe('0.12'); + }); + + it('value值会把number类型转为字符串,且.xx转为0.xx', () => { + Inula.render(, container); + expect(container.querySelector('input').hasAttribute('value')).toBe(true); + expect(container.querySelector('input').getAttribute('value')).toBe('0.12'); + }); + + it('改变node.value值', () => { + let setNum; + const App = () => { + const [num, _setNum] = Inula.useState(''); + setNum = _setNum; + return ; + }; + Inula.render(, container); + expect(container.querySelector('input').hasAttribute('value')).toBe(true); + expect(container.querySelector('input').getAttribute('value')).toBe(''); + act(() => { + setNum(0); + }); + expect(container.querySelector('input').value).toBe('0'); + }); + + it('node.value精度', () => { + let setNum; + const App = () => { + const [num, _setNum] = Inula.useState(0.0); + setNum = _setNum; + return ; + }; + Inula.render(, container); + expect(container.querySelector('input').getAttribute('value')).toBe('0'); + act(() => { + setNum(1.0); + }); + expect(container.querySelector('input').getAttribute('value')).toBe('0'); + expect(container.querySelector('input').value).toBe('1'); + }); + + it('node.value与Attrubute value', () => { + const App = () => { + return ; + }; + Inula.render(, container); + expect(container.querySelector('input').getAttribute('value')).toBe('1'); + expect(container.querySelector('input').value).toBe('1'); + + // 先修改 + Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set.call( + container.querySelector('input'), + '8' + ); + // 再触发事件 + container.querySelector('input').dispatchEvent( + new Event('input', { + bubbles: true, + cancelable: true, + }) + ); + // Attrubute value不会改变,node.value会改变 + expect(container.querySelector('input').getAttribute('value')).toBe('1'); + expect(container.querySelector('input').value).toBe('8'); + }); + }); + + describe('type reset', () => { + it('type reset的value值', () => { + Inula.render(, container); + expect(container.querySelector('input').hasAttribute('value')).toBe(true); + expect(container.querySelector('input').getAttribute('value')).toBe('0.12'); + + Inula.unmountComponentAtNode(container); + Inula.render(, container); + expect(container.querySelector('input').hasAttribute('value')).toBe(true); + expect(container.querySelector('input').getAttribute('value')).toBe(''); + + Inula.unmountComponentAtNode(container); + Inula.render(, container); + expect(container.querySelector('input').hasAttribute('value')).toBe(false); + }); + }); +}); diff --git a/packages/inula-reactive/scripts/__tests__/DomTest/DomSelect.test.js b/packages/inula-reactive/scripts/__tests__/DomTest/DomSelect.test.js new file mode 100644 index 00000000..596a9225 --- /dev/null +++ b/packages/inula-reactive/scripts/__tests__/DomTest/DomSelect.test.js @@ -0,0 +1,336 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import * as Inula from '../../../src/index'; + +describe('Dom Select', () => { + it('设置value', () => { + const selectNode = ( + + ); + const realNode = Inula.render(selectNode, container); + expect(realNode.value).toBe('Vue'); + expect(realNode.options[1].selected).toBe(true); + realNode.value = 'React'; + // 改变value会影响select的状态 + Inula.render(selectNode, container); + expect(realNode.options[0].selected).toBe(true); + expect(realNode.value).toBe('React'); + }); + + it('设置value为对象', () => { + let selectValue = { + toString: () => { + return 'Vue'; + }, + }; + const selectNode = ( + + ); + const realNode = Inula.render(selectNode, container); + expect(realNode.value).toBe('Vue'); + expect(realNode.options[1].selected).toBe(true); + selectValue = { + toString: () => { + return 'React'; + }, + }; + const newSelectNode = ( + + ); + // 改变value会影响select的状态 + Inula.render(newSelectNode, container); + expect(realNode.options[0].selected).toBe(true); + expect(realNode.value).toBe('React'); + }); + + it('受控select转为不受控会保存原来select', () => { + const selectNode = ( + + ); + const realNode = Inula.render(selectNode, container); + expect(realNode.value).toBe('Vue'); + expect(realNode.options[1].selected).toBe(true); + const newSelectNode = ( + + ); + Inula.render(newSelectNode, container); + // selected不变 + expect(realNode.options[0].selected).toBe(false); + expect(realNode.options[1].selected).toBe(true); + expect(realNode.value).toBe('Vue'); + }); + + it('设置defaultValue', () => { + let defaultVal = 'Vue'; + const selectNode = ( + + ); + let realNode = Inula.render(selectNode, container); + expect(realNode.value).toBe('Vue'); + expect(realNode.options[1].selected).toBe(true); + + defaultVal = 'React'; + // 改变defaultValue没有影响 + realNode = Inula.render(selectNode, container); + expect(realNode.value).toBe('Vue'); + expect(realNode.options[0].selected).toBe(false); + expect(realNode.options[1].selected).toBe(true); + }); + + it('设置defaultValue后,select不受控', () => { + const selectNode = ( + + ); + let realNode = Inula.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生效,select不受控 + Inula.render(selectNode, container); + // 'React'项没被选中 + expect(realNode.options[0].selected).toBe(true); + expect(realNode.options[1].selected).toBe(false); + expect(realNode.value).toBe('React'); + }); + + xit('设置multiple(一)', () => { + jest.spyOn(console, 'error').mockImplementation(); + const selectNode = ( + + ); + expect(() => Inula.render(selectNode, container)).toThrowError('newValues.forEach is not a function'); + }); + + it('设置multiple(二)', () => { + let selectNode = ( + + ); + expect(() => Inula.render(selectNode, container)).not.toThrow(); + expect(document.getElementById('se').options[0].selected).toBe(false); + expect(document.getElementById('se').options[1].selected).toBe(true); + expect(document.getElementById('se').options[2].selected).toBe(true); + + // 改变defaultValue没有影响 + selectNode = ( + + ); + Inula.render(selectNode, container); + expect(document.getElementById('se').options[0].selected).toBe(false); + expect(document.getElementById('se').options[1].selected).toBe(true); + expect(document.getElementById('se').options[2].selected).toBe(true); + }); + + it('设置multiple(三)', () => { + let selectNode = ( + + ); + expect(() => Inula.render(selectNode, container)).not.toThrow(); + expect(document.getElementById('se').options[0].selected).toBe(false); + expect(document.getElementById('se').options[1].selected).toBe(true); + expect(document.getElementById('se').options[2].selected).toBe(true); + + // 改变value有影响 + selectNode = ( + + ); + Inula.render(selectNode, container); + expect(document.getElementById('se').options[0].selected).toBe(true); + expect(document.getElementById('se').options[1].selected).toBe(false); + expect(document.getElementById('se').options[2].selected).toBe(false); + }); + + it('defaultValue设置multiple与非multiple切换(一)', () => { + let selectNode = ( + + ); + Inula.render(selectNode, container); + expect(document.getElementById('se').options[0].selected).toBe(false); + expect(document.getElementById('se').options[1].selected).toBe(true); + expect(document.getElementById('se').options[2].selected).toBe(true); + + // 改变value有影响 + selectNode = ( + + ); + Inula.render(selectNode, container); + expect(document.getElementById('se').options[0].selected).toBe(true); + expect(document.getElementById('se').options[1].selected).toBe(false); + expect(document.getElementById('se').options[2].selected).toBe(false); + }); + + it('defaultValue设置multiple与非multiple切换(二)', () => { + let selectNode = ( + + ); + Inula.render(selectNode, container); + expect(document.getElementById('se').options[0].selected).toBe(true); + expect(document.getElementById('se').options[1].selected).toBe(false); + expect(document.getElementById('se').options[2].selected).toBe(false); + + // 改变value有影响 + selectNode = ( + + ); + Inula.render(selectNode, container); + expect(document.getElementById('se').options[0].selected).toBe(false); + expect(document.getElementById('se').options[1].selected).toBe(true); + expect(document.getElementById('se').options[2].selected).toBe(true); + }); + + it('未指定value或者defaultValue时,默认选择第一个可选的', () => { + const selectNode = ( + + ); + const realNode = Inula.render(selectNode, container); + expect(realNode.options[0].selected).toBe(false); + expect(realNode.options[1].selected).toBe(true); + expect(realNode.options[2].selected).toBe(false); + }); + + it('删除添加option', () => { + const selectNode = ( + + ); + const realNode = Inula.render(selectNode, container); + expect(realNode.options[0].selected).toBe(false); + expect(realNode.options[1].selected).toBe(true); + expect(realNode.options[2].selected).toBe(false); + + const newNode = ( + + ); + Inula.render(newNode, container); + expect(realNode.options[0].selected).toBe(false); + expect(realNode.options[1].selected).toBe(false); + + const newSelectNode = ( + + ); + // 重新添加不会影响 + Inula.render(newSelectNode, container); + expect(realNode.options[0].selected).toBe(false); + expect(realNode.options[1].selected).toBe(false); + expect(realNode.options[2].selected).toBe(false); + }); +}); diff --git a/packages/inula-reactive/scripts/__tests__/DomTest/DomTextarea.test.js b/packages/inula-reactive/scripts/__tests__/DomTest/DomTextarea.test.js new file mode 100644 index 00000000..7f793b01 --- /dev/null +++ b/packages/inula-reactive/scripts/__tests__/DomTest/DomTextarea.test.js @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import * as Inula from '../../../src/index'; + +describe('Dom Textarea', () => { + it('设置value', () => { + let realNode = Inula.render(, container); + expect(realNode.value).toBe('false'); + }); + + it('设置defaultValue为对象', () => { + let textareaValue = { + toString: () => { + return 'Vue'; + }, + }; + const textareaNode = , container); + expect(realNode.value).toBe('1234'); + realNode = Inula.render(, container); + // realNode.value依旧为1234 + expect(realNode.value).toBe('1234'); + }); +}); diff --git a/packages/inula-reactive/scripts/__tests__/EventTest/EventMain.test.js b/packages/inula-reactive/scripts/__tests__/EventTest/EventMain.test.js new file mode 100644 index 00000000..c7bdcb74 --- /dev/null +++ b/packages/inula-reactive/scripts/__tests__/EventTest/EventMain.test.js @@ -0,0 +1,281 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import * as Inula from '../../../src/index'; +import * as TestUtils from '../jest/testUtils'; + +function dispatchChangeEvent(input) { + const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set; + nativeInputValueSetter.call(input, 'test'); + + input.dispatchEvent(new Event('input', { bubbles: true })); +} + +describe('事件', () => { + const LogUtils = TestUtils.getLogUtils(); + + it('事件捕获与冒泡', () => { + const App = () => { + return ( + <> +
LogUtils.log('div capture')} onClick={() => LogUtils.log('div bubble')}> +

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

+ + ); + }; + Inula.render(, container); + const a = container.querySelector('button'); + a.click(); + expect(LogUtils.getAndClear()).toEqual([ + // 从外到内先捕获再冒泡 + 'div capture', + 'p capture', + 'btn capture', + 'btn bubble', + 'p bubble', + 'div bubble', + ]); + }); + + it('returns 0', () => { + let keyCode = null; + const node = Inula.render( + { + keyCode = e.keyCode; + }} + />, + container + ); + node.dispatchEvent( + new KeyboardEvent('keypress', { + keyCode: 65, + bubbles: true, + cancelable: true, + }) + ); + expect(keyCode).toBe(65); + }); + + it('阻止事件冒泡', () => { + const App = () => { + return ( + <> +
LogUtils.log('div capture')} onClick={() => LogUtils.log('div bubble')}> +

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

+ + ); + }; + Inula.render(, container); + container.querySelector('button').click(); + + expect(LogUtils.getAndClear()).toEqual([ + // 到button时停止冒泡 + 'div capture', + 'p capture', + 'btn capture', + 'btn bubble', + ]); + }); + + it('阻止事件捕获', () => { + const App = () => { + return ( + <> +
TestUtils.stopBubbleOrCapture(e, 'div capture')} + onClick={() => LogUtils.log('div bubble')} + > +

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

+ + ); + }; + Inula.render(, container); + container.querySelector('button').click(); + + expect(LogUtils.getAndClear()).toEqual([ + // 阻止捕获,不再继续向下执行 + 'div capture', + ]); + }); + + it('阻止原生事件冒泡', () => { + const App = () => { + return ( +
+

+

+ ); + }; + Inula.render(, container); + container.querySelector('div').addEventListener( + 'click', + () => { + LogUtils.log('div bubble'); + }, + false + ); + container.querySelector('p').addEventListener( + 'click', + () => { + LogUtils.log('p bubble'); + }, + false + ); + container.querySelector('button').addEventListener( + 'click', + e => { + LogUtils.log('btn bubble'); + e.stopPropagation(); + }, + false + ); + container.querySelector('button').click(); + expect(LogUtils.getAndClear()).toEqual(['btn bubble']); + }); + + it('动态增加事件', () => { + let update; + let inputRef = Inula.createRef(); + + function Test() { + const [inputProps, setProps] = Inula.useState({}); + update = setProps; + return ; + } + + Inula.render(, container); + update({ + onChange: () => { + LogUtils.log('change'); + }, + }); + dispatchChangeEvent(inputRef.current); + + expect(LogUtils.getAndClear()).toEqual(['change']); + }); + + it('Radio change事件', () => { + let radio1Called = 0; + let radio2Called = 0; + + function onChange1() { + radio1Called++; + } + + function onChange2() { + radio2Called++; + } + + const radio1Ref = Inula.createRef(); + const radio2Ref = Inula.createRef(); + + Inula.render( + <> + + + , + container + ); + + function clickRadioAndExpect(radio, [expect1, expect2]) { + radio.click(); + expect(radio1Called).toBe(expect1); + expect(radio2Called).toBe(expect2); + } + + // 先选择选项1 + clickRadioAndExpect(radio1Ref.current, [1, 0]); + + // 再选择选项1 + clickRadioAndExpect(radio2Ref.current, [1, 1]); + + // 先选择选项1,radio1应该重新触发onchange + clickRadioAndExpect(radio1Ref.current, [2, 1]); + }); + + it('多根节点下,事件挂载正确', () => { + const root1 = document.createElement('div'); + const root2 = document.createElement('div'); + root1.key = 'root1'; + root2.key = 'root2'; + let input1, input2, update1, update2; + + function App1() { + const [props, setProps] = Inula.useState({}); + update1 = setProps; + return ( + (input1 = n)} + onChange={() => { + LogUtils.log('input1 changed'); + }} + /> + ); + } + + function App2() { + const [props, setProps] = Inula.useState({}); + update2 = setProps; + + return ( + (input2 = n)} + onChange={() => { + LogUtils.log('input2 changed'); + }} + /> + ); + } + + // 多根mount阶段挂载onChange事件 + Inula.render(, root1); + Inula.render(, root2); + + dispatchChangeEvent(input1); + expect(LogUtils.getAndClear()).toEqual(['input1 changed']); + dispatchChangeEvent(input2); + expect(LogUtils.getAndClear()).toEqual(['input2 changed']); + + // 多根update阶段挂载onClick事件 + update1({ + onClick: () => LogUtils.log('input1 clicked'), + }); + update2({ + onClick: () => LogUtils.log('input2 clicked'), + }); + + input1.click(); + expect(LogUtils.getAndClear()).toEqual(['input1 clicked']); + input2.click(); + expect(LogUtils.getAndClear()).toEqual(['input2 clicked']); + }); +}); diff --git a/packages/inula-reactive/scripts/__tests__/EventTest/FocusEvent.test.js b/packages/inula-reactive/scripts/__tests__/EventTest/FocusEvent.test.js new file mode 100644 index 00000000..c201ad43 --- /dev/null +++ b/packages/inula-reactive/scripts/__tests__/EventTest/FocusEvent.test.js @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import * as Inula from '../../../src/index'; +import { getLogUtils } from '../jest/testUtils'; + +describe('合成焦点事件', () => { + const LogUtils = getLogUtils(); + + it('onFocus', () => { + const realNode = Inula.render( + LogUtils.log(`onFocus: ${event.type}`)} + onFocusCapture={event => LogUtils.log(`onFocusCapture: ${event.type}`)} + />, + container + ); + + realNode.dispatchEvent( + new FocusEvent('focusin', { + bubbles: true, + cancelable: false, + }) + ); + + expect(LogUtils.getAndClear()).toEqual(['onFocusCapture: focus', 'onFocus: focus']); + }); + + it('onBlur', () => { + const realNode = Inula.render( + LogUtils.log(`onBlur: ${event.type}`)} + onBlurCapture={event => LogUtils.log(`onBlurCapture: ${event.type}`)} + />, + container + ); + + realNode.dispatchEvent( + new FocusEvent('focusout', { + bubbles: true, + cancelable: false, + }) + ); + + expect(LogUtils.getAndClear()).toEqual(['onBlurCapture: blur', 'onBlur: blur']); + }); +}); diff --git a/packages/inula-reactive/scripts/__tests__/EventTest/KeyboardEvent.test.js b/packages/inula-reactive/scripts/__tests__/EventTest/KeyboardEvent.test.js new file mode 100644 index 00000000..5a4780f8 --- /dev/null +++ b/packages/inula-reactive/scripts/__tests__/EventTest/KeyboardEvent.test.js @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import * as Inula from '../../../src/index'; +import { getLogUtils } from '../jest/testUtils'; + +describe('Keyboard Event', () => { + const LogUtils = getLogUtils(); + const getKeyboardEvent = (type, keyCode, code, charCode) => { + return new KeyboardEvent(type, { + keyCode: keyCode ?? undefined, + code: code ?? undefined, + charCode: charCode ?? undefined, + bubbles: true, + cancelable: true, + }); + }; + + it('keydown,keypress,keyup的keycode,charcode', () => { + const node = Inula.render( + { + LogUtils.log('onKeyUp: keycode: ' + e.keyCode + ',charcode: ' + e.charCode); + }} + onKeyDown={e => { + LogUtils.log('onKeyDown: keycode: ' + e.keyCode + ',charcode: ' + e.charCode); + }} + />, + container + ); + node.dispatchEvent(getKeyboardEvent('keydown', 50, 'Digit2')); + node.dispatchEvent(getKeyboardEvent('keyup', 50, 'Digit2')); + + expect(LogUtils.getAndClear()).toEqual(['onKeyDown: keycode: 50,charcode: 0', 'onKeyUp: keycode: 50,charcode: 0']); + }); + + it('keypress的keycode,charcode', () => { + const node = Inula.render( + { + LogUtils.log('onKeyPress: keycode: ' + e.keyCode + ',charcode: ' + e.charCode); + }} + />, + container + ); + node.dispatchEvent(getKeyboardEvent('keypress', undefined, 'Digit2', 50)); + + expect(LogUtils.getAndClear()).toEqual(['onKeyPress: keycode: 0,charcode: 50']); + }); + + it('当charcode为13,且不设置keycode的时候', () => { + const node = Inula.render( + { + LogUtils.log('onKeyPress: keycode: ' + e.keyCode + ',charcode: ' + e.charCode); + }} + />, + container + ); + node.dispatchEvent(getKeyboardEvent('keypress', undefined, undefined, 13)); + expect(LogUtils.getAndClear()).toEqual(['onKeyPress: keycode: 0,charcode: 13']); + }); + + it('keydown,keypress,keyup的code', () => { + const node = Inula.render( + { + LogUtils.log('onKeyUp: code: ' + e.code); + }} + onKeyPress={e => { + LogUtils.log('onKeyPress: code: ' + e.code); + }} + onKeyDown={e => { + LogUtils.log('onKeyDown: code: ' + e.code); + }} + />, + container + ); + node.dispatchEvent(getKeyboardEvent('keydown', undefined, 'Digit2')); + node.dispatchEvent(getKeyboardEvent('keypress', undefined, 'Digit2', 50)); + + node.dispatchEvent( + new KeyboardEvent('keyup', { + code: 'Digit2', + bubbles: true, + cancelable: true, + }) + ); + + expect(LogUtils.getAndClear()).toEqual([ + 'onKeyDown: code: Digit2', + 'onKeyPress: code: Digit2', + 'onKeyUp: code: Digit2', + ]); + }); + + it('可以执行preventDefault和 stopPropagation', () => { + const keyboardProcessing = e => { + expect(e.isDefaultPrevented()).toBe(false); + e.preventDefault(); + expect(e.isDefaultPrevented()).toBe(true); + + expect(e.isPropagationStopped()).toBe(false); + e.stopPropagation(); + expect(e.isPropagationStopped()).toBe(true); + LogUtils.log(e.type + ' handle'); + }; + const div = Inula.render( +
, + container + ); + div.dispatchEvent(getKeyboardEvent('keydown', 40)); + div.dispatchEvent(getKeyboardEvent('keyup', 40)); + div.dispatchEvent(getKeyboardEvent('keypress', 40)); + + expect(LogUtils.getAndClear()).toEqual(['keydown handle', 'keyup handle', 'keypress handle']); + }); +}); diff --git a/packages/inula-reactive/scripts/__tests__/EventTest/MouseEnterEvent.test.js b/packages/inula-reactive/scripts/__tests__/EventTest/MouseEnterEvent.test.js new file mode 100644 index 00000000..870cdbc1 --- /dev/null +++ b/packages/inula-reactive/scripts/__tests__/EventTest/MouseEnterEvent.test.js @@ -0,0 +1,278 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import * as Inula from '../../../src/index'; + +describe('mouseenter和mouseleave事件测试', () => { + let container; + + beforeEach(() => { + jest.resetModules(); + container = document.createElement('div'); + document.body.appendChild(container); + }); + + afterEach(() => { + document.body.removeChild(container); + container = null; + }); + + it('在iframe中mouseleave事件的relateTarget属性', () => { + const iframe = document.createElement('iframe'); + container.appendChild(iframe); + const iframeDocument = iframe.contentDocument; + iframeDocument.write('
'); + iframeDocument.close(); + + const leaveEvents = []; + const node = Inula.render( +
{ + e.persist(); + leaveEvents.push(e); + }} + />, + iframeDocument.body.getElementsByTagName('div')[0] + ); + + node.dispatchEvent( + new MouseEvent('mouseout', { + bubbles: true, + cancelable: true, + relatedTarget: iframe.contentWindow, + }) + ); + + expect(leaveEvents.length).toBe(1); + expect(leaveEvents[0].target).toBe(node); + expect(leaveEvents[0].relatedTarget).toBe(iframe.contentWindow); + }); + + it('在iframe中mouseenter事件的relateTarget属性', () => { + const iframe = document.createElement('iframe'); + container.appendChild(iframe); + const iframeDocument = iframe.contentDocument; + iframeDocument.write('
'); + iframeDocument.close(); + + const enterEvents = []; + const node = Inula.render( +
{ + e.persist(); + enterEvents.push(e); + }} + />, + iframeDocument.body.getElementsByTagName('div')[0] + ); + + node.dispatchEvent( + new MouseEvent('mouseover', { + bubbles: true, + cancelable: true, + relatedTarget: null, + }) + ); + + expect(enterEvents.length).toBe(1); + expect(enterEvents[0].target).toBe(node); + expect(enterEvents[0].relatedTarget).toBe(iframe.contentWindow); + }); + + it('从新渲染的子组件触发mouseout事件,子组件响应mouseenter事件,父节点不响应', () => { + let parentEnterCalls = 0; + let childEnterCalls = 0; + let parent = null; + + class Parent extends Inula.Component { + render() { + return ( +
parentEnterCalls++} ref={node => (parent = node)}> + {this.props.showChild &&
childEnterCalls++} />} +
+ ); + } + } + + Inula.render(, container); + Inula.render(, container); + + parent.dispatchEvent( + new MouseEvent('mouseout', { + bubbles: true, + cancelable: true, + relatedTarget: parent.firstChild, + }) + ); + expect(childEnterCalls).toBe(1); + expect(parentEnterCalls).toBe(0); + }); + + it('render一个新组件,兄弟节点触发mouseout事件,mouseenter事件响应一次', done => { + const mockFn1 = jest.fn(); + const mockFn2 = jest.fn(); + const mockFn3 = jest.fn(); + + class Parent extends Inula.Component { + constructor(props) { + super(props); + this.parentEl = Inula.createRef(); + } + + componentDidMount() { + Inula.render(, this.parentEl.current); + } + + render() { + return
; + } + } + + class MouseEnterDetect extends Inula.Component { + constructor(props) { + super(props); + this.firstEl = Inula.createRef(); + this.siblingEl = Inula.createRef(); + } + + componentDidMount() { + this.siblingEl.current.dispatchEvent( + new MouseEvent('mouseout', { + bubbles: true, + cancelable: true, + relatedTarget: this.firstEl.current, + }) + ); + expect(mockFn1.mock.calls.length).toBe(1); + expect(mockFn2.mock.calls.length).toBe(1); + expect(mockFn3.mock.calls.length).toBe(0); + done(); + } + + render() { + return ( + +
+
+ + ); + } + } + + Inula.render(, container); + }); + + it('未被inula管理的节点触发mouseout事件,mouseenter事件也能正常触发', done => { + const mockFn = jest.fn(); + + class Parent extends Inula.Component { + constructor(props) { + super(props); + this.parentEl = Inula.createRef(); + } + + componentDidMount() { + Inula.render(, this.parentEl.current); + } + + render() { + return
; + } + } + + class MouseEnterDetect extends Inula.Component { + constructor(props) { + super(props); + this.divRef = Inula.createRef(); + this.siblingEl = Inula.createRef(); + } + + componentDidMount() { + const attachedNode = document.createElement('div'); + this.divRef.current.appendChild(attachedNode); + attachedNode.dispatchEvent( + new MouseEvent('mouseout', { + bubbles: true, + cancelable: true, + relatedTarget: this.siblingEl.current, + }) + ); + expect(mockFn.mock.calls.length).toBe(1); + done(); + } + + render() { + return ( +
+
+
+ ); + } + } + + Inula.render(, container); + }); + + it('外部portal节点触发的mouseout事件,根节点的mouseleave事件也能响应', () => { + const divRef = Inula.createRef(); + const onMouseLeave = jest.fn(); + + function Component() { + return ( +
+ {Inula.createPortal(
, document.body)} +
+ ); + } + + Inula.render(, container); + + divRef.current.dispatchEvent( + new MouseEvent('mouseout', { + bubbles: true, + cancelable: true, + relatedTarget: document.body, + }) + ); + + expect(onMouseLeave).toHaveBeenCalledTimes(1); + }); + + it('外部portal节点触发的mouseout事件,根节点的mouseEnter事件也能响应', () => { + const divRef = Inula.createRef(); + const otherDivRef = Inula.createRef(); + const onMouseEnter = jest.fn(); + + function Component() { + return ( +
+ {Inula.createPortal(
, document.body)} +
+ ); + } + + Inula.render(, container); + + divRef.current.dispatchEvent( + new MouseEvent('mouseout', { + bubbles: true, + cancelable: true, + relatedTarget: otherDivRef.current, + }) + ); + + expect(onMouseEnter).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/inula-reactive/scripts/__tests__/EventTest/MouseEvent.test.js b/packages/inula-reactive/scripts/__tests__/EventTest/MouseEvent.test.js new file mode 100644 index 00000000..a840c3d4 --- /dev/null +++ b/packages/inula-reactive/scripts/__tests__/EventTest/MouseEvent.test.js @@ -0,0 +1,185 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import * as Inula from '../../../src/index'; +import { getLogUtils } from '../jest/testUtils'; + +describe('MouseEvent Test', () => { + const LogUtils = getLogUtils(); + + describe('onClick Test', () => { + it('绑定this', () => { + class App extends Inula.Component { + constructor(props) { + super(props); + this.state = { + num: this.props.num, + price: this.props.price, + }; + } + + setNum() { + this.setState({ num: this.state.num + 1 }); + } + + setPrice = e => { + this.setState({ num: this.state.price + 1 }); + }; + + render() { + return ( + <> +

{this.state.num}

+

{this.state.price}

+ + + + ); + } + } + + Inula.render(, container); + expect(container.querySelector('p').innerHTML).toBe('0'); + expect(container.querySelector('#p').innerHTML).toBe('100'); + // 点击按钮触发num加1 + container.querySelector('button').click(); + expect(container.querySelector('p').innerHTML).toBe('1'); + + container.querySelector('#btn').click(); + expect(container.querySelector('p').innerHTML).toBe('101'); + }); + + it('点击触发', () => { + const handleClick = jest.fn(); + Inula.render(, container); + container.querySelector('button').click(); + expect(handleClick).toHaveBeenCalledTimes(1); + for (let i = 0; i < 5; i++) { + container.querySelector('button').click(); + } + expect(handleClick).toHaveBeenCalledTimes(6); + }); + + it('disable不触发click', () => { + const handleClick = jest.fn(); + const spanRef = Inula.createRef(); + Inula.render( + , + container + ); + spanRef.current.click(); + + expect(handleClick).toHaveBeenCalledTimes(0); + }); + }); + + const test = (name, config) => { + const node = Inula.render(config, container); + let event = new MouseEvent(name, { + relatedTarget: null, + bubbles: true, + screenX: 1, + }); + node.dispatchEvent(event); + + expect(LogUtils.getAndClear()).toEqual([`${name} capture`, `${name} bubble`]); + + event = new MouseEvent(name, { + relatedTarget: null, + bubbles: true, + screenX: 2, + }); + node.dispatchEvent(event); + + // 再次触发新事件 + expect(LogUtils.getAndClear()).toEqual([`${name} capture`, `${name} bubble`]); + }; + + describe('合成鼠标事件', () => { + it('onMouseMove', () => { + const onMouseMove = () => { + LogUtils.log('mousemove bubble'); + }; + const onMouseMoveCapture = () => { + LogUtils.log('mousemove capture'); + }; + test('mousemove',
); + }); + + it('onMouseDown', () => { + const onMousedown = () => { + LogUtils.log('mousedown bubble'); + }; + const onMousedownCapture = () => { + LogUtils.log('mousedown capture'); + }; + test('mousedown',
); + }); + + it('onMouseUp', () => { + const onMouseUp = () => { + LogUtils.log('mouseup bubble'); + }; + const onMouseUpCapture = () => { + LogUtils.log('mouseup capture'); + }; + test('mouseup',
); + }); + + it('onMouseOut', () => { + const onMouseOut = () => { + LogUtils.log('mouseout bubble'); + }; + const onMouseOutCapture = () => { + LogUtils.log('mouseout capture'); + }; + test('mouseout',
); + }); + + it('onMouseOver', () => { + const onMouseOver = () => { + LogUtils.log('mouseover bubble'); + }; + const onMouseOverCapture = () => { + LogUtils.log('mouseover capture'); + }; + test('mouseover',
); + }); + + it('KeyboardEvent.getModifierState should not fail', () => { + const input = Inula.render( + { + e.getModifierState('CapsLock'); + }} + />, + container + ); + const event = new MouseEvent('mousedown', { + relatedTarget: null, + bubbles: true, + screenX: 1, + }); + + expect(() => { + input.dispatchEvent(event); + }).not.toThrow(); + }); + }); +}); diff --git a/packages/inula-reactive/scripts/__tests__/EventTest/WheelEvent.test.js b/packages/inula-reactive/scripts/__tests__/EventTest/WheelEvent.test.js new file mode 100644 index 00000000..02b404a1 --- /dev/null +++ b/packages/inula-reactive/scripts/__tests__/EventTest/WheelEvent.test.js @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import * as Inula from '../../../src/index'; +import { getLogUtils } from '../jest/testUtils'; + +describe('合成滚轮事件', () => { + const LogUtils = getLogUtils(); + + it('onWheel', () => { + const realNode = Inula.render( +
LogUtils.log(`onWheel: ${event.type}`)} + onWheelCapture={event => LogUtils.log(`onWheelCapture: ${event.type}`)} + />, + container + ); + + realNode.dispatchEvent( + new MouseEvent('wheel', { + bubbles: true, + cancelable: false, + }) + ); + + expect(LogUtils.getAndClear()).toEqual(['onWheelCapture: wheel', 'onWheel: wheel']); + }); + + it('可以执行preventDefault和stopPropagation', () => { + const eventHandler = e => { + expect(e.isDefaultPrevented()).toBe(false); + e.preventDefault(); + expect(e.isDefaultPrevented()).toBe(true); + + expect(e.isPropagationStopped()).toBe(false); + e.stopPropagation(); + expect(e.isPropagationStopped()).toBe(true); + LogUtils.log(e.type + ' handle'); + }; + const realNode = Inula.render(
, container); + + realNode.dispatchEvent( + new MouseEvent('wheel', { + bubbles: true, + cancelable: true, + }) + ); + expect(LogUtils.getAndClear()).toEqual(['wheel handle']); + }); +}); diff --git a/packages/inula-reactive/scripts/__tests__/HorizonXTest/StateManager/StateArray.test.tsx b/packages/inula-reactive/scripts/__tests__/HorizonXTest/StateManager/StateArray.test.tsx new file mode 100644 index 00000000..065fdfd6 --- /dev/null +++ b/packages/inula-reactive/scripts/__tests__/HorizonXTest/StateManager/StateArray.test.tsx @@ -0,0 +1,223 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +//@ts-ignore +import * as Inula from '../../../../src/index'; +import * as LogUtils from '../../jest/logUtils'; +import { clearStore, createStore, useStore } from '../../../../src/inulax/store/StoreHandler'; +import { App, Text, triggerClickEvent } from '../../jest/commonComponents'; +import { describe, beforeEach, afterEach, it, expect } from '@jest/globals'; + +const useUserStore = createStore({ + id: 'user', + state: { + type: 'bing dun dun', + persons: [ + { name: 'p1', age: 1 }, + { name: 'p2', age: 2 }, + ], + }, + actions: { + addOnePerson: (state, person) => { + state.persons.push(person); + }, + delOnePerson: state => { + state.persons.pop(); + }, + clearPersons: state => { + state.persons = []; + }, + reset: state => { + state.persons = [ + { name: 'p1', age: 1 }, + { name: 'p2', age: 2 }, + ]; + }, + }, +}); + +describe('测试store中的Array', () => { + const { unmountComponentAtNode } = Inula; + let container: HTMLElement | null = null; + beforeEach(() => { + // 创建一个 DOM 元素作为渲染目标 + container = document.createElement('div'); + document.body.appendChild(container); + + useUserStore().reset(); + }); + + afterEach(() => { + // 退出时进行清理 + unmountComponentAtNode(container); + container?.remove(); + container = null; + LogUtils.clear(); + + clearStore('user'); + }); + + const newPerson = { name: 'p3', age: 3 }; + function Parent(props) { + const userStore = useUserStore(); + const addOnePerson = function () { + userStore.addOnePerson(newPerson); + }; + const delOnePerson = function () { + userStore.delOnePerson(); + }; + return ( +
+ + +
{props.children}
+
+ ); + } + + it('测试Array方法: push()、pop()', () => { + function Child(props) { + const userStore = useUserStore(); + + return ( +
+ +
+ ); + } + + Inula.render(, container); + + expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: 2'); + // 在Array中增加一个对象 + Inula.act(() => { + triggerClickEvent(container, 'addBtn'); + }); + expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: 3'); + + // 在Array中删除一个对象 + Inula.act(() => { + triggerClickEvent(container, 'delBtn'); + }); + expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: 2'); + }); + + it('测试Array方法: entries()、push()、shift()、unshift、直接赋值', () => { + let globalStore = useUserStore(); + function Child(props) { + const userStore = useUserStore(); + + const nameList: string[] = []; + const entries = userStore.$s.persons?.entries(); + if (entries) { + for (const entry of entries) { + nameList.push(entry[1].name); + } + } + + return ( +
+ +
+ ); + } + + Inula.render(, container); + + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2'); + // push + globalStore.$s.persons.push(newPerson); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2 p3'); + + // shift + //@ts-ignore TODO:why is this argument here? + globalStore.$s.persons.shift({ name: 'p0', age: 0 }); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p2 p3'); + + // 赋值[2] + globalStore.$s.persons[2] = { name: 'p4', age: 4 }; + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p2 p3 p4'); + + // 重新赋值[2] + globalStore.$s.persons[2] = { name: 'p5', age: 5 }; + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p2 p3 p5'); + + // unshift + globalStore.$s.persons.unshift({ name: 'p1', age: 1 }); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2 p3 p5'); + + // 重新赋值 [] + globalStore.$s.persons = []; + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: '); + + // 重新赋值 [{ name: 'p1', age: 1 }] + globalStore.$s.persons = [{ name: 'p1', age: 1 }]; + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1'); + }); + + it('测试Array方法: forEach()', () => { + let globalStore = useUserStore(); + function Child(props) { + const userStore = useUserStore(); + + const nameList: string[] = []; + userStore.$s.persons?.forEach(per => { + nameList.push(per.name); + }); + + return ( +
+ +
+ ); + } + + Inula.render(, container); + + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2'); + // push + globalStore.$s.persons.push(newPerson); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2 p3'); + + // shift + //@ts-ignore TODO: why is this argument here? + globalStore.$s.persons.shift({ name: 'p0', age: 0 }); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p2 p3'); + + // 赋值[2] + globalStore.$s.persons[2] = { name: 'p4', age: 4 }; + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p2 p3 p4'); + + // 重新赋值[2] + globalStore.$s.persons[2] = { name: 'p5', age: 5 }; + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p2 p3 p5'); + + // unshift + globalStore.$s.persons.unshift({ name: 'p1', age: 1 }); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2 p3 p5'); + + // 重新赋值 [] + globalStore.$s.persons = []; + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: '); + + // 重新赋值 [{ name: 'p1', age: 1 }] + globalStore.$s.persons = [{ name: 'p1', age: 1 }]; + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1'); + }); +}); diff --git a/packages/inula-reactive/scripts/__tests__/HorizonXTest/StateManager/StateMap.test.tsx b/packages/inula-reactive/scripts/__tests__/HorizonXTest/StateManager/StateMap.test.tsx new file mode 100644 index 00000000..fc81b61b --- /dev/null +++ b/packages/inula-reactive/scripts/__tests__/HorizonXTest/StateManager/StateMap.test.tsx @@ -0,0 +1,348 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +//@ts-ignore +import * as Inula from '../../../../src/index'; +import * as LogUtils from '../../jest/logUtils'; +import { clearStore, createStore, useStore } from '../../../../src/inulax/store/StoreHandler'; +import { App, Text, triggerClickEvent } from '../../jest/commonComponents'; +import { describe, beforeEach, afterEach, it, expect } from '@jest/globals'; + +const useUserStore = createStore({ + id: 'user', + state: { + type: 'bing dun dun', + persons: new Map([ + ['p1', 1], + ['p2', 2], + ]), + }, + actions: { + addOnePerson: (state, person) => { + state.persons.set(person.name, person.age); + }, + delOnePerson: (state, person) => { + state.persons.delete(person.name); + }, + clearPersons: state => { + state.persons.clear(); + }, + reset: state => { + state.persons = new Map([ + ['p1', 1], + ['p2', 2], + ]); + }, + }, +}); + +describe('测试store中的Map', () => { + const { unmountComponentAtNode } = Inula; + let container: HTMLElement | null = null; + beforeEach(() => { + // 创建一个 DOM 元素作为渲染目标 + container = document.createElement('div'); + document.body.appendChild(container); + + useUserStore().reset(); + }); + + afterEach(() => { + // 退出时进行清理 + unmountComponentAtNode(container); + container?.remove(); + container = null; + LogUtils.clear(); + + clearStore('user'); + }); + + const newPerson = { name: 'p3', age: 3 }; + + function Parent(props) { + const userStore = useUserStore(); + 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 = useUserStore(); + + return ( +
+ +
+ ); + } + + Inula.render(, container); + + expect(container?.querySelector('#size')?.innerHTML).toBe('persons number: 2'); + // 在Map中增加一个对象 + Inula.act(() => { + triggerClickEvent(container, 'addBtn'); + }); + expect(container?.querySelector('#size')?.innerHTML).toBe('persons number: 3'); + + // 在Map中删除一个对象 + Inula.act(() => { + triggerClickEvent(container, 'delBtn'); + }); + expect(container?.querySelector('#size')?.innerHTML).toBe('persons number: 2'); + + // clear Map + Inula.act(() => { + triggerClickEvent(container, 'clearBtn'); + }); + expect(container?.querySelector('#size')?.innerHTML).toBe('persons number: 0'); + }); + + it('测试Map方法: keys()', () => { + function Child(props) { + const userStore = useUserStore(); + + const nameList: string[] = []; + const keys = userStore.$s.persons.keys(); + for (const key of keys) { + nameList.push(key); + } + + return ( +
+ +
+ ); + } + + Inula.render(, container); + + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2'); + // 在Map中增加一个对象 + Inula.act(() => { + triggerClickEvent(container, 'addBtn'); + }); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2 p3'); + + // 在Map中删除一个对象 + Inula.act(() => { + triggerClickEvent(container, 'delBtn'); + }); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2'); + + // clear Map + Inula.act(() => { + triggerClickEvent(container, 'clearBtn'); + }); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: '); + }); + + it('测试Map方法: values()', () => { + function Child(props) { + const userStore = useUserStore(); + + const ageList: number[] = []; + const values = userStore.$s.persons.values(); + for (const val of values) { + ageList.push(val); + } + + return ( +
+ +
+ ); + } + + Inula.render(, container); + + expect(container?.querySelector('#ageList')?.innerHTML).toBe('age list: 1 2'); + // 在Map中增加一个对象 + Inula.act(() => { + triggerClickEvent(container, 'addBtn'); + }); + expect(container?.querySelector('#ageList')?.innerHTML).toBe('age list: 1 2 3'); + + // 在Map中删除一个对象 + Inula.act(() => { + triggerClickEvent(container, 'delBtn'); + }); + expect(container?.querySelector('#ageList')?.innerHTML).toBe('age list: 1 2'); + + // clear Map + Inula.act(() => { + triggerClickEvent(container, 'clearBtn'); + }); + expect(container?.querySelector('#ageList')?.innerHTML).toBe('age list: '); + }); + + it('测试Map方法: entries()', () => { + function Child(props) { + const userStore = useUserStore(); + + const nameList: string[] = []; + const entries = userStore.$s.persons.entries(); + for (const entry of entries) { + nameList.push(entry[0]); + } + + return ( +
+ +
+ ); + } + + Inula.render(, container); + + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2'); + // 在Map中增加一个对象 + Inula.act(() => { + triggerClickEvent(container, 'addBtn'); + }); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2 p3'); + + // 在Map中删除一个对象 + Inula.act(() => { + triggerClickEvent(container, 'delBtn'); + }); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2'); + + // clear Map + Inula.act(() => { + triggerClickEvent(container, 'clearBtn'); + }); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: '); + }); + + it('测试Map方法: forEach()', () => { + function Child(props) { + const userStore = useUserStore(); + + const nameList: string[] = []; + userStore.$s.persons.forEach((val, key) => { + nameList.push(key); + }); + + return ( +
+ +
+ ); + } + + Inula.render(, container); + + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2'); + // 在Map中增加一个对象 + Inula.act(() => { + triggerClickEvent(container, 'addBtn'); + }); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2 p3'); + + // 在Map中删除一个对象 + Inula.act(() => { + triggerClickEvent(container, 'delBtn'); + }); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2'); + + // clear Map + Inula.act(() => { + triggerClickEvent(container, 'clearBtn'); + }); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: '); + }); + + it('测试Map方法: has()', () => { + function Child(props) { + const userStore = useUserStore(); + + return ( +
+ +
+ ); + } + + Inula.render(, container); + + expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: false'); + // 在Map中增加一个对象 + Inula.act(() => { + triggerClickEvent(container, 'addBtn'); + }); + expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: true'); + }); + + it('测试Map方法: for of()', () => { + function Child(props) { + const userStore = useUserStore(); + + const nameList: string[] = []; + for (const per of userStore.$s.persons) { + nameList.push(per[0]); + } + + return ( +
+ +
+ ); + } + + Inula.render(, container); + + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2'); + // 在Map中增加一个对象 + Inula.act(() => { + triggerClickEvent(container, 'addBtn'); + }); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2 p3'); + + // 在Map中删除一个对象 + Inula.act(() => { + triggerClickEvent(container, 'delBtn'); + }); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2'); + + // clear Map + Inula.act(() => { + triggerClickEvent(container, 'clearBtn'); + }); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: '); + }); +}); diff --git a/packages/inula-reactive/scripts/__tests__/HorizonXTest/StateManager/StateMixType.test.tsx b/packages/inula-reactive/scripts/__tests__/HorizonXTest/StateManager/StateMixType.test.tsx new file mode 100644 index 00000000..413969af --- /dev/null +++ b/packages/inula-reactive/scripts/__tests__/HorizonXTest/StateManager/StateMixType.test.tsx @@ -0,0 +1,177 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +//@ts-ignore +import * as Inula from '../../../../src/index'; +import * as LogUtils from '../../jest/logUtils'; +import { clearStore, createStore, useStore } from '../../../../src/inulax/store/StoreHandler'; +import { App, Text, triggerClickEvent } from '../../jest/commonComponents'; + +describe('测试store中的混合类型变化', () => { + const { unmountComponentAtNode } = Inula; + let container: HTMLElement | null = 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 as HTMLElement).remove(); + container = null; + LogUtils.clear(); + + 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.persons.values().next().value.love.get('lanqiu').days; + + return ( +
+ +
+ ); + } + + Inula.render(, container); + + expect(container?.querySelector('#dayList')?.innerHTML).toBe('love: 1 3 5'); + Inula.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: string[] = []; + const valIterator = userStore.persons.values(); + let per = valIterator.next() as { + value: { + name: string; + getName: () => string; + }; + done: boolean; + }; + while (!per.done) { + nameList.push(per.value.name ?? per.value.getName()); + globalPerson = per.value; + per = valIterator.next(); + } + + return ( +
+ +
+ ); + } + + Inula.render(, container); + + expect(container?.querySelector('#nameList')?.innerHTML).toBe('p1 p2'); + + // 动态增加一个Person实例 + globalStore.$s.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/packages/inula-reactive/scripts/__tests__/HorizonXTest/StateManager/StateSet.test.tsx b/packages/inula-reactive/scripts/__tests__/HorizonXTest/StateManager/StateSet.test.tsx new file mode 100644 index 00000000..0fc657f9 --- /dev/null +++ b/packages/inula-reactive/scripts/__tests__/HorizonXTest/StateManager/StateSet.test.tsx @@ -0,0 +1,318 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +//@ts-ignore +import * as Inula from '../../../../src/index'; +import * as LogUtils from '../../jest/logUtils'; +import { clearStore, createStore, useStore } from '../../../../src/inulax/store/StoreHandler'; +import { App, Text, triggerClickEvent } from '../../jest/commonComponents'; +import { describe, beforeEach, afterEach, it, expect } from '@jest/globals'; + +const useUserStore = createStore({ + id: 'user', + state: { + type: 'bing dun dun', + persons: new Set([ + { name: 'p1', age: 1 }, + { name: 'p2', age: 2 }, + ]), + }, + actions: { + addOnePerson: (state, person) => { + state.persons.add(person); + }, + delOnePerson: (state, person) => { + state.persons.delete(person); + }, + clearPersons: state => { + state.persons.clear(); + }, + reset: state => { + state.persons = new Set([ + { name: 'p1', age: 1 }, + { name: 'p2', age: 2 }, + ]); + }, + }, +}); + +describe('测试store中的Set', () => { + const { unmountComponentAtNode } = Inula; + let container: HTMLElement | null = null; + beforeEach(() => { + // 创建一个 DOM 元素作为渲染目标 + container = document.createElement('div'); + document.body.appendChild(container); + + useUserStore().reset(); + }); + + afterEach(() => { + // 退出时进行清理 + unmountComponentAtNode(container); + container?.remove(); + container = null; + LogUtils.clear(); + + clearStore('user'); + }); + + const newPerson = { name: 'p3', age: 3 }; + function Parent(props) { + const userStore = useUserStore(); + 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 = useUserStore(); + const personArr = Array.from(userStore.$s.persons); + const nameList: string[] = []; + const keys = userStore.$s.persons.keys(); + for (const key of keys) { + nameList.push(key.name); + } + + return ( +
+ + +
+ ); + } + + Inula.render(, container); + + expect(container?.querySelector('#size')?.innerHTML).toBe('persons number: 2'); + expect(container?.querySelector('#lastAge')?.innerHTML).toBe('last person age: 2'); + // 在set中增加一个对象 + Inula.act(() => { + triggerClickEvent(container, 'addBtn'); + }); + expect(container?.querySelector('#size')?.innerHTML).toBe('persons number: 3'); + + // 在set中删除一个对象 + Inula.act(() => { + triggerClickEvent(container, 'delBtn'); + }); + expect(container?.querySelector('#size')?.innerHTML).toBe('persons number: 2'); + + // clear set + Inula.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 = useUserStore(); + + const nameList: string[] = []; + const keys = userStore.$s.persons.keys(); + // const keys = userStore.$s.persons.values(); + for (const key of keys) { + nameList.push(key.name); + } + + return ( +
+ +
+ ); + } + + Inula.render(, container); + + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2'); + // 在set中增加一个对象 + Inula.act(() => { + triggerClickEvent(container, 'addBtn'); + }); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2 p3'); + + // 在set中删除一个对象 + Inula.act(() => { + triggerClickEvent(container, 'delBtn'); + }); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2'); + + // clear set + Inula.act(() => { + triggerClickEvent(container, 'clearBtn'); + }); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: '); + }); + + it('测试Set方法: entries()', () => { + function Child(props) { + const userStore = useUserStore(); + + const nameList: string[] = []; + const entries = userStore.$s.persons.entries(); + for (const entry of entries) { + nameList.push(entry[0].name); + } + + return ( +
+ +
+ ); + } + + Inula.render(, container); + + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2'); + // 在set中增加一个对象 + Inula.act(() => { + triggerClickEvent(container, 'addBtn'); + }); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2 p3'); + + // 在set中删除一个对象 + Inula.act(() => { + triggerClickEvent(container, 'delBtn'); + }); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2'); + + // clear set + Inula.act(() => { + triggerClickEvent(container, 'clearBtn'); + }); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: '); + }); + + it('测试Set方法: forEach()', () => { + function Child(props) { + const userStore = useUserStore(); + + const nameList: string[] = []; + userStore.$s.persons.forEach(per => { + nameList.push(per.name); + }); + + return ( +
+ +
+ ); + } + + Inula.render(, container); + + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2'); + // 在set中增加一个对象 + Inula.act(() => { + triggerClickEvent(container, 'addBtn'); + }); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2 p3'); + + // 在set中删除一个对象 + Inula.act(() => { + triggerClickEvent(container, 'delBtn'); + }); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2'); + + // clear set + Inula.act(() => { + triggerClickEvent(container, 'clearBtn'); + }); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: '); + }); + + it('测试Set方法: has()', () => { + function Child(props) { + const userStore = useUserStore(); + + return ( +
+ +
+ ); + } + + Inula.render(, container); + + expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: false'); + // 在set中增加一个对象 + Inula.act(() => { + triggerClickEvent(container, 'addBtn'); + }); + expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: true'); + }); + + it('测试Set方法: for of()', () => { + function Child(props) { + const userStore = useUserStore(); + + const nameList: string[] = []; + for (const per of userStore.$s.persons) { + nameList.push(per.name); + } + + return ( +
+ +
+ ); + } + + Inula.render(, container); + + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2'); + // 在set中增加一个对象 + Inula.act(() => { + triggerClickEvent(container, 'addBtn'); + }); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2 p3'); + + // 在set中删除一个对象 + Inula.act(() => { + triggerClickEvent(container, 'delBtn'); + }); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2'); + + // clear set + Inula.act(() => { + triggerClickEvent(container, 'clearBtn'); + }); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: '); + }); +}); diff --git a/packages/inula-reactive/scripts/__tests__/HorizonXTest/StateManager/StateWeakMap.test.tsx b/packages/inula-reactive/scripts/__tests__/HorizonXTest/StateManager/StateWeakMap.test.tsx new file mode 100644 index 00000000..2cccd515 --- /dev/null +++ b/packages/inula-reactive/scripts/__tests__/HorizonXTest/StateManager/StateWeakMap.test.tsx @@ -0,0 +1,149 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +//@ts-ignore +import * as Inula from '../../../../src/index'; +import * as LogUtils from '../../jest/logUtils'; +import { clearStore, createStore, useStore } from '../../../../src/inulax/store/StoreHandler'; +import { App, Text, triggerClickEvent } from '../../jest/commonComponents'; +import { describe, beforeEach, afterEach, it, expect } from '@jest/globals'; + +const useUserStore = createStore({ + id: 'user', + state: { + type: 'bing dun dun', + persons: new WeakMap([ + [{ name: 'p1' }, 1], + [{ name: 'p2' }, 2], + ]), + }, + actions: { + addOnePerson: (state, person) => { + state.persons.set(person, 3); + }, + delOnePerson: (state, person) => { + state.persons.delete(person); + }, + clearPersons: state => { + state.persons = new WeakMap([]); + }, + reset: state => { + state.persons = new WeakMap([ + [{ name: 'p1' }, 1], + [{ name: 'p2' }, 2], + ]); + }, + }, +}); + +describe('测试store中的WeakMap', () => { + const { unmountComponentAtNode } = Inula; + let container: HTMLElement | null = null; + beforeEach(() => { + // 创建一个 DOM 元素作为渲染目标 + container = document.createElement('div'); + document.body.appendChild(container); + + useUserStore().reset(); + }); + + afterEach(() => { + // 退出时进行清理 + unmountComponentAtNode(container); + container?.remove(); + container = null; + LogUtils.clear(); + + clearStore('user'); + }); + + const newPerson = { name: 'p3' }; + + function Parent(props) { + const userStore = useUserStore(); + 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 = useUserStore(); + + return ( +
+ +
+ ); + } + + Inula.render(, container); + + expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: false'); + // 在WeakMap中增加一个对象 + Inula.act(() => { + triggerClickEvent(container, 'addBtn'); + }); + expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: true'); + + // 在WeakMap中删除一个对象 + Inula.act(() => { + triggerClickEvent(container, 'delBtn'); + }); + expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: false'); + }); + + it('测试WeakMap方法: get()', () => { + function Child(props) { + const userStore = useUserStore(); + + return ( +
+ +
+ ); + } + + Inula.render(, container); + + expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: undefined'); + // 在WeakMap中增加一个对象 + Inula.act(() => { + triggerClickEvent(container, 'addBtn'); + }); + expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: 3'); + }); +}); diff --git a/packages/inula-reactive/scripts/__tests__/HorizonXTest/StateManager/StateWeakSet.test.tsx b/packages/inula-reactive/scripts/__tests__/HorizonXTest/StateManager/StateWeakSet.test.tsx new file mode 100644 index 00000000..6e2f322f --- /dev/null +++ b/packages/inula-reactive/scripts/__tests__/HorizonXTest/StateManager/StateWeakSet.test.tsx @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +//@ts-ignore +import * as Inula from '../../../../src/index'; +import * as LogUtils from '../../jest/logUtils'; +import { clearStore, createStore, useStore } from '../../../../src/inulax/store/StoreHandler'; +import { App, Text, triggerClickEvent } from '../../jest/commonComponents'; +import { describe, beforeEach, afterEach, it, expect } from '@jest/globals'; + +const useUserStore = createStore({ + id: 'user', + state: { + type: 'bing dun dun', + persons: new WeakSet([ + { name: 'p1', age: 1 }, + { name: 'p2', age: 2 }, + ]), + }, + actions: { + addOnePerson: (state, person) => { + state.persons.add(person); + }, + delOnePerson: (state, person) => { + state.persons.delete(person); + }, + clearPersons: state => { + state.persons = new WeakSet([]); + }, + reset: state => { + state.persons = new WeakSet([ + { name: 'p1', age: 1 }, + { name: 'p2', age: 2 }, + ]); + }, + }, +}); + +describe('测试store中的WeakSet', () => { + const { unmountComponentAtNode } = Inula; + let container: HTMLElement | null = null; + beforeEach(() => { + // 创建一个 DOM 元素作为渲染目标 + container = document.createElement('div'); + document.body.appendChild(container); + + useUserStore().reset(); + }); + + afterEach(() => { + // 退出时进行清理 + unmountComponentAtNode(container); + container?.remove(); + container = null; + LogUtils.clear(); + + clearStore('user'); + }); + + const newPerson = { name: 'p3', age: 3 }; + function Parent(props) { + const userStore = useUserStore(); + 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 = useUserStore(); + + return ( +
+ +
+ ); + } + + Inula.render(, container); + + expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: false'); + // 在WeakSet中增加一个对象 + Inula.act(() => { + triggerClickEvent(container, 'addBtn'); + }); + expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: true'); + + // 在WeakSet中删除一个对象 + Inula.act(() => { + triggerClickEvent(container, 'delBtn'); + }); + expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: false'); + }); +}); diff --git a/packages/inula-reactive/scripts/__tests__/HorizonXTest/StoreFunctionality/async.test.tsx b/packages/inula-reactive/scripts/__tests__/HorizonXTest/StoreFunctionality/async.test.tsx new file mode 100644 index 00000000..157dea0c --- /dev/null +++ b/packages/inula-reactive/scripts/__tests__/HorizonXTest/StoreFunctionality/async.test.tsx @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +//@ts-ignore +import * as Inula from '../../../../src/index'; +import { createStore } from '../../../../src/inulax/store/StoreHandler'; +import { triggerClickEvent } from '../../jest/commonComponents'; +import { describe, beforeEach, afterEach, it, expect } from '@jest/globals'; + +const { unmountComponentAtNode } = Inula; + +function postpone(timer, func) { + return new Promise(resolve => { + window.setTimeout(function () { + console.log('resolving postpone'); + resolve(func()); + }, timer); + }); +} + +describe('Asynchronous store', () => { + const getStore = createStore({ + state: { + counter: 0, + check: false, + }, + actions: { + increment: function (state) { + return new Promise(resolve => { + window.setTimeout(() => { + state.counter++; + resolve(true); + }, 10); + }); + }, + toggle: function (state) { + state.check = !state.check; + }, + reset: function (state) { + state.check = false; + state.counter = 0; + }, + }, + computed: { + value: state => { + return (state.check ? 'true' : 'false') + state.counter; + }, + }, + }); + + beforeEach(() => { + getStore().reset(); + }); + + it('should return promise when queued function is called', () => { + jest.useFakeTimers(); + + const store = getStore(); + + return new Promise(resolve => { + store.$queue.increment().then(() => { + expect(store.counter == 1); + resolve(true); + }); + + jest.advanceTimersByTime(150); + }); + }); + + it('should queue async functions', () => { + jest.useFakeTimers(); + return new Promise(resolve => { + const store = getStore(); + + // initial value + expect(store.value).toBe('false0'); + + // no blocking action action + store.$queue.toggle(); + expect(store.value).toBe('true0'); + + // store is not updated before blocking action is resolved + store.$queue.increment(); + const togglePromise = store.$queue.toggle(); + expect(store.value).toBe('true0'); + + // fast action is resolved immediatelly + store.toggle(); + expect(store.value).toBe('false0'); + + // queued action waits for blocking action to resolve + togglePromise.then(() => { + expect(store.value).toBe('true1'); + resolve(); + }); + + jest.advanceTimersByTime(150); + }); + }); +}); diff --git a/packages/inula-reactive/scripts/__tests__/HorizonXTest/StoreFunctionality/basicAccess.test.tsx b/packages/inula-reactive/scripts/__tests__/HorizonXTest/StoreFunctionality/basicAccess.test.tsx new file mode 100644 index 00000000..e12de714 --- /dev/null +++ b/packages/inula-reactive/scripts/__tests__/HorizonXTest/StoreFunctionality/basicAccess.test.tsx @@ -0,0 +1,198 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +//@ts-ignore +import Inula from '../../../../src/index'; +import { triggerClickEvent } from '../../jest/commonComponents'; +import { useLogStore } from './store'; +import { describe, beforeEach, afterEach, it, expect } from '@jest/globals'; +import { createStore } from '../../../../src/inulax/store/StoreHandler'; + +const { unmountComponentAtNode } = Inula; + +describe('Basic store manipulation', () => { + let container: HTMLElement | null = 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}
; + } + + Inula.render(, container); + + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('1'); + }); + + it('Should use direct setters', () => { + function App() { + const logStore = useLogStore(); + + return ( +
+ +

{logStore.logs[0]}

+
+ ); + } + + Inula.render(, container); + + Inula.act(() => { + triggerClickEvent(container, BUTTON_ID); + }); + + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('q'); + }); + + it('Should use actions and update components', () => { + function App() { + const logStore = useLogStore(); + + return ( +
+ +

{logStore.length}

+
+ ); + } + + Inula.render(, container); + + Inula.act(() => { + triggerClickEvent(container, BUTTON_ID); + }); + + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('2'); + }); + + it('should call actions from own actions', () => { + const useIncrementStore = createStore({ + id: 'incrementStore', + state: { + count: 2, + }, + actions: { + increment: state => { + state.count++; + }, + doublePlusOne: function (state) { + state.count = state.count * 2; + this.increment(); + }, + }, + }); + + function App() { + const incrementStore = useIncrementStore(); + + return ( +
+ +

{incrementStore.count}

+
+ ); + } + + Inula.render(, container); + + Inula.act(() => { + triggerClickEvent(container, BUTTON_ID); + }); + + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('5'); + }); + + it('should call computed from own actions', () => { + const useIncrementStore = createStore({ + id: 'incrementStore', + state: { + count: 2, + }, + actions: { + doublePlusOne: function (state) { + state.count = this.double + 1; + }, + }, + computed: { + double: state => { + return state.count * 2; + }, + }, + }); + + function App() { + const incrementStore = useIncrementStore(); + + return ( +
+ +

{incrementStore.count}

+
+ ); + } + + Inula.render(, container); + + Inula.act(() => { + triggerClickEvent(container, BUTTON_ID); + }); + + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('5'); + }); +}); diff --git a/packages/inula-reactive/scripts/__tests__/HorizonXTest/StoreFunctionality/cloneDeep.test.js b/packages/inula-reactive/scripts/__tests__/HorizonXTest/StoreFunctionality/cloneDeep.test.js new file mode 100644 index 00000000..340bd488 --- /dev/null +++ b/packages/inula-reactive/scripts/__tests__/HorizonXTest/StoreFunctionality/cloneDeep.test.js @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import * as Inula from '../../../../src/index'; +import { clearStore, createStore, useStore } from '../../../../src/inulax/store/StoreHandler'; +import { OBSERVER_KEY } from '../../../../src/inulax/Constants'; +import { App, Text, triggerClickEvent } from '../../jest/commonComponents'; + +describe('测试对store.state对象进行深度克隆', () => { + const { unmountComponentAtNode } = Inula; + let container = null; + beforeEach(() => { + // 创建一个 DOM 元素作为渲染目标 + container = document.createElement('div'); + document.body.appendChild(container); + + createStore({ + id: 'user', + state: { + type: 'bing dun dun', + persons: [ + { name: 'p1', age: 1 }, + { name: 'p2', age: 2 }, + ], + }, + actions: { + addOnePerson: (state, person) => { + state.persons.push(person); + }, + delOnePerson: state => { + state.persons.pop(); + }, + clearPersons: state => { + state.persons = null; + }, + }, + }); + }); + + afterEach(() => { + // 退出时进行清理 + unmountComponentAtNode(container); + container.remove(); + container = null; + + clearStore('user'); + }); + + const newPerson = { name: 'p3', age: 3 }; + + function Parent({ children }) { + const userStore = useStore('user'); + const addOnePerson = function () { + userStore.addOnePerson(newPerson); + }; + const delOnePerson = function () { + userStore.delOnePerson(); + }; + return ( +
+ + +
{children}
+
+ ); + } + + it("The observer object of symbol ('_inulaObserver') cannot be accessed to from Proxy", () => { + let userStore = null; + function Child(props) { + userStore = useStore('user'); + + return ( +
+ +
+ ); + } + + Inula.render(, container); + + // The observer object of symbol ('_inulaObserver') cannot be accessed to from Proxy prevent errors caused by clonedeep. + expect(userStore.persons[0][OBSERVER_KEY]).toBe(undefined); + }); + + it("The observer object of symbol ('_inulaObserver') cannot be accessed to from Proxy", () => { + let userStore = null; + function Child(props) { + userStore = useStore('user'); + + return ( +
+ +
+ ); + } + + Inula.render(, container); + + // NO throw this Exception, TypeError: 'get' on proxy: property 'prototype' is a read-only and non-configurable data property on the proxy target but the proxy did not return its actual value + const proxyObj = userStore.persons[0].constructor; + expect(proxyObj.prototype !== undefined).toBeTruthy(); + }); +}); diff --git a/packages/inula-reactive/scripts/__tests__/HorizonXTest/StoreFunctionality/dollarAccess.test.tsx b/packages/inula-reactive/scripts/__tests__/HorizonXTest/StoreFunctionality/dollarAccess.test.tsx new file mode 100644 index 00000000..61edc1d7 --- /dev/null +++ b/packages/inula-reactive/scripts/__tests__/HorizonXTest/StoreFunctionality/dollarAccess.test.tsx @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +//@ts-ignore +import * as Inula from '../../../../src/index'; +import { triggerClickEvent } from '../../jest/commonComponents'; +import { useLogStore } from './store'; +import { describe, beforeEach, afterEach, it, expect } from '@jest/globals'; + +const { unmountComponentAtNode } = Inula; + +describe('Dollar store access', () => { + let container: HTMLElement | null = 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 $s and $c', () => { + function App() { + const logStore = useLogStore(); + + return
{logStore.$c.length()}
; + } + + Inula.render(, container); + + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('1'); + }); + + it('Should use $a and update components', () => { + function App() { + const logStore = useLogStore(); + + return ( +
+ +

{logStore.$c.length()}

+
+ ); + } + + Inula.render(, container); + + Inula.act(() => { + triggerClickEvent(container, BUTTON_ID); + }); + + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('2'); + }); +}); diff --git a/packages/inula-reactive/scripts/__tests__/HorizonXTest/StoreFunctionality/otherCases.test.tsx b/packages/inula-reactive/scripts/__tests__/HorizonXTest/StoreFunctionality/otherCases.test.tsx new file mode 100644 index 00000000..b2ebc57e --- /dev/null +++ b/packages/inula-reactive/scripts/__tests__/HorizonXTest/StoreFunctionality/otherCases.test.tsx @@ -0,0 +1,165 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +//@ts-ignore +import * as Inula from '../../../../src/index'; +import { createStore } from '../../../../src/inulax/store/StoreHandler'; +import { triggerClickEvent } from '../../jest/commonComponents'; +import { describe, beforeEach, afterEach, it, expect } from '@jest/globals'; + +const { unmountComponentAtNode } = Inula; + +describe('Self referencing', () => { + let container: HTMLElement | null = null; + + const BUTTON_ID = 'btn'; + const RESULT_ID = 'result'; + + const useSelfRefStore = createStore({ + state: { + val: 2, + }, + actions: { + increaseVal: 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}

+ +
+ ); + } + + Inula.render(, container); + + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('4'); + + Inula.act(() => { + triggerClickEvent(container, BUTTON_ID); + }); + + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('6'); + + Inula.act(() => { + triggerClickEvent(container, BUTTON_ID); + }); + + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('10'); + }); + + it('should access other stores', () => { + const useOtherStore = createStore({ + state: {}, + actions: { + doIncreaseVal: () => useSelfRefStore().increaseVal(), + }, + computed: { + selfRefStoreValue: () => useSelfRefStore().value, + }, + }); + + function App() { + const store = useOtherStore(); + + return ( +
+

{store.selfRefStoreValue}

+ +
+ ); + } + + Inula.render(, container); + + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('5'); + + Inula.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)}

+ +
+ ); + } + + Inula.render(, container); + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('abc'); + + Inula.act(() => { + triggerClickEvent(container, BUTTON_ID); + }); + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('def'); + }); +}); diff --git a/packages/inula-reactive/scripts/__tests__/HorizonXTest/StoreFunctionality/reset.js b/packages/inula-reactive/scripts/__tests__/HorizonXTest/StoreFunctionality/reset.js new file mode 100644 index 00000000..828a856e --- /dev/null +++ b/packages/inula-reactive/scripts/__tests__/HorizonXTest/StoreFunctionality/reset.js @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import * as Inula from '../../../../src/index'; +import { createStore } from '../../../../src/inulax/store/StoreHandler'; +import { triggerClickEvent } from '../../jest/commonComponents'; + +const { unmountComponentAtNode } = Inula; + +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.$s.counter}

+ + +
+ ); + } + + Inula.render(, container); + + Inula.act(() => { + triggerClickEvent(container, BUTTON_ID); + }); + + Inula.act(() => { + triggerClickEvent(container, BUTTON_ID); + }); + + expect(document.getElementById(RESULT_ID).innerHTML).toBe('2'); + + Inula.act(() => { + triggerClickEvent(container, RESET_ID); + }); + + expect(document.getElementById(RESULT_ID).innerHTML).toBe('0'); + + Inula.act(() => { + triggerClickEvent(container, BUTTON_ID); + }); + + expect(document.getElementById(RESULT_ID).innerHTML).toBe('1'); + }); +}); diff --git a/packages/inula-reactive/scripts/__tests__/HorizonXTest/StoreFunctionality/store.ts b/packages/inula-reactive/scripts/__tests__/HorizonXTest/StoreFunctionality/store.ts new file mode 100644 index 00000000..be33234c --- /dev/null +++ b/packages/inula-reactive/scripts/__tests__/HorizonXTest/StoreFunctionality/store.ts @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { createStore } from '../../../../src/inulax/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/packages/inula-reactive/scripts/__tests__/HorizonXTest/StoreFunctionality/utils.test.js b/packages/inula-reactive/scripts/__tests__/HorizonXTest/StoreFunctionality/utils.test.js new file mode 100644 index 00000000..1d544784 --- /dev/null +++ b/packages/inula-reactive/scripts/__tests__/HorizonXTest/StoreFunctionality/utils.test.js @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { resolveMutation } from '../../../../src/inulax/CommonUtils'; + +describe('Mutation resolve', () => { + it('should resolve mutation different types', () => { + const mutation = resolveMutation(null, 42); + + expect(mutation.mutation).toBe(true); + expect(mutation.from).toBe(null); + expect(mutation.to).toBe(42); + }); + + it('should resolve mutation same type types, different values', () => { + const mutation = resolveMutation(13, 42); + + expect(mutation.mutation).toBe(true); + expect(mutation.from).toBe(13); + expect(mutation.to).toBe(42); + }); + + it('should resolve mutation same type types, same values', () => { + const mutation = resolveMutation(42, 42); + + expect(mutation.mutation).toBe(false); + expect(Object.keys(mutation).length).toBe(1); + }); + + it('should resolve mutation same type types, same objects', () => { + const mutation = resolveMutation({ a: { b: { c: 1 } } }, { a: { b: { c: 1 } } }); + + expect(mutation.mutation).toBe(false); + }); + + it('should resolve mutation same type types, same array', () => { + const mutation = resolveMutation([1, 2, 3, 4, 5], [1, 2, 3, 4, 5]); + + expect(mutation.mutation).toBe(false); + }); + + it('should resolve mutation same type types, longer array', () => { + const mutation = resolveMutation([1, 2, 3, 4, 5], [1, 2, 3, 4, 5, 6]); + + expect(mutation.mutation).toBe(true); + expect(mutation.items[5].mutation).toBe(true); + expect(mutation.items[5].to).toBe(6); + }); + + it('should resolve mutation same type types, shorter array', () => { + const mutation = resolveMutation([1, 2, 3, 4, 5], [1, 2, 3, 4]); + + expect(mutation.mutation).toBe(true); + expect(mutation.items[4].mutation).toBe(true); + expect(mutation.items[4].from).toBe(5); + }); + + it('should resolve mutation same type types, changed array', () => { + const mutation = resolveMutation([1, 2, 3, 4, 5], [1, 2, 3, 4, 'a']); + + expect(mutation.mutation).toBe(true); + expect(mutation.items[4].mutation).toBe(true); + expect(mutation.items[4].from).toBe(5); + expect(mutation.items[4].to).toBe('a'); + }); + + it('should resolve mutation same type types, same object', () => { + const mutation = resolveMutation({ a: 1, b: 2 }, { a: 1, b: 2 }); + + expect(mutation.mutation).toBe(false); + }); + + it('should resolve mutation same type types, changed object', () => { + const mutation = resolveMutation({ a: 1, b: 2, c: 3 }, { a: 1, c: 2 }); + + expect(mutation.mutation).toBe(true); + expect(mutation.attributes.a.mutation).toBe(false); + expect(mutation.attributes.b.mutation).toBe(true); + expect(mutation.attributes.b.from).toBe(2); + expect(mutation.attributes.c.to).toBe(2); + }); +}); + +describe('Mutation collections', () => { + it('should resolve mutation of two sets', () => { + const values = [{ a: 1 }, { b: 2 }, { c: 3 }]; + + const source = new Set([values[0], values[1], values[2]]); + + const target = new Set([values[0], values[1]]); + + const mutation = resolveMutation(source, target); + + expect(mutation.mutation).toBe(true); + }); +}); diff --git a/packages/inula-reactive/scripts/__tests__/HorizonXTest/StoreFunctionality/watch.test.tsx b/packages/inula-reactive/scripts/__tests__/HorizonXTest/StoreFunctionality/watch.test.tsx new file mode 100644 index 00000000..f63c2ff3 --- /dev/null +++ b/packages/inula-reactive/scripts/__tests__/HorizonXTest/StoreFunctionality/watch.test.tsx @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { createStore } from '../../../../src/index'; +import { watch } from '../../../../src/index'; + +describe('watch', () => { + it('shouhld watch primitive 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); + }); + + 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; + + store.arr.watch('0', () => { + counter++; + }); + + store.change(); + + 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/packages/inula-reactive/scripts/__tests__/HorizonXTest/adapters/ReduxAdapter.test.tsx b/packages/inula-reactive/scripts/__tests__/HorizonXTest/adapters/ReduxAdapter.test.tsx new file mode 100644 index 00000000..109ccbc2 --- /dev/null +++ b/packages/inula-reactive/scripts/__tests__/HorizonXTest/adapters/ReduxAdapter.test.tsx @@ -0,0 +1,234 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +//@ts-ignore +import * as Inula from '../../../../src/index'; +import { + createStore, + applyMiddleware, + combineReducers, + bindActionCreators, +} from '../../../../src/inulax/adapters/redux'; +import { describe, it, expect } from '@jest/globals'; + +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: string[] = []; + + 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: string[] = []; + + 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/packages/inula-reactive/scripts/__tests__/HorizonXTest/adapters/ReduxAdapterThunk.test.tsx b/packages/inula-reactive/scripts/__tests__/HorizonXTest/adapters/ReduxAdapterThunk.test.tsx new file mode 100644 index 00000000..559e159f --- /dev/null +++ b/packages/inula-reactive/scripts/__tests__/HorizonXTest/adapters/ReduxAdapterThunk.test.tsx @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import * as Inula from '../../../../src/index'; +import { createStore, applyMiddleware, thunk } from '../../../../src/inulax/adapters/redux'; +import { describe, it, expect } from '@jest/globals'; + +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++) { + //TODO: resolve thunk problems + (todoStore.dispatch as unknown as (delayedAction: (dispatch, getState) => void) => void)( + addTodosIfAllowed('todo no.' + i) + ); + } + + expect(todoStore.getState().todos.length).toBe(5); + }); +}); diff --git a/packages/inula-reactive/scripts/__tests__/HorizonXTest/adapters/ReduxReactAdapter.test.tsx b/packages/inula-reactive/scripts/__tests__/HorizonXTest/adapters/ReduxReactAdapter.test.tsx new file mode 100644 index 00000000..9c284bd3 --- /dev/null +++ b/packages/inula-reactive/scripts/__tests__/HorizonXTest/adapters/ReduxReactAdapter.test.tsx @@ -0,0 +1,378 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +//@ts-ignore +import * as Inula from '../../../../src/index'; +import { + batch, + connect, + createStore, + Provider, + useDispatch, + useSelector, + useStore, + createSelectorHook, + createDispatchHook, +} from '../../../../src/inulax/adapters/redux'; +import { triggerClickEvent } from '../../jest/commonComponents'; +import { describe, it, beforeEach, afterEach, expect } from '@jest/globals'; +import { ReduxStoreHandler } from '../../../../src/inulax/adapters/redux'; + +const BUTTON = 'button'; +const BUTTON2 = 'button2'; +const RESULT = 'result'; +const CONTAINER = 'container'; + +function getE(id): HTMLElement { + return document.getElementById(id) || document.body; +} + +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() as unknown as ReduxStoreHandler; + return
{store.getState()}
; + }; + + const Wrapper = () => { + return ( + + + + ); + }; + + Inula.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() as unknown as ReduxStoreHandler; + const dispatch = useDispatch(); + return ( +
+

{store.getState()}

+ +
+ ); + }; + + const Wrapper = () => { + return ( + + + + ); + }; + + Inula.render(, getE(CONTAINER)); + + expect(reduxStore.getState()).toBe(0); + + Inula.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 ( + + + + ); + }; + + Inula.render(, getE(CONTAINER)); + + expect(getE(RESULT).innerHTML).toBe('0'); + + Inula.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 { + // @ts-ignore + 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 = () => { + //@ts-ignore + const [amount, setAmount] = Inula.useState(5); + return ( + + + + + ); + }; + + Inula.render(, getE(CONTAINER)); + + expect(getE(RESULT).innerHTML).toBe('+0'); + + Inula.act(() => { + triggerClickEvent(getE(CONTAINER), BUTTON); + }); + + expect(getE(RESULT).innerHTML).toBe('+5'); + + Inula.act(() => { + triggerClickEvent(getE(CONTAINER), BUTTON2); + }); + + Inula.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}

+ +
+ ); + } + + Inula.render( + + + , + getE(CONTAINER) + ); + + expect(getE(RESULT).innerHTML).toBe('0'); + expect(renderCounter).toBe(1); + + Inula.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 = Inula.createContext(); + const toggleContext = Inula.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 ( +
+ + + + + + + +
+ ); + } + + Inula.render(, getE(CONTAINER)); + + expect(getE(BUTTON).innerHTML).toBe('0'); + expect(getE(BUTTON2).innerHTML).toBe('false'); + + Inula.act(() => { + triggerClickEvent(getE(CONTAINER), BUTTON); + triggerClickEvent(getE(CONTAINER), BUTTON2); + }); + + expect(getE(BUTTON).innerHTML).toBe('1'); + expect(getE(BUTTON2).innerHTML).toBe('true'); + }); +}); diff --git a/packages/inula-reactive/scripts/__tests__/HorizonXTest/adapters/connectTest.tsx b/packages/inula-reactive/scripts/__tests__/HorizonXTest/adapters/connectTest.tsx new file mode 100644 index 00000000..408de658 --- /dev/null +++ b/packages/inula-reactive/scripts/__tests__/HorizonXTest/adapters/connectTest.tsx @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { createElement } from '../../../../src/external/JSXElement'; +import { createDomTextVNode } from '../../../../src/renderer/vnode/VNodeCreator'; +import { createStore } from '../../../../src/inulax/adapters/redux'; +import { connect } from '../../../../src/inulax/adapters/reduxReact'; + +createStore((state: number = 0, action): number => { + if (action.type === 'add') return state + 1; + return 0; +}); + +type WrappedButtonProps = { add: () => void; count: number; text: string }; + +function Button(props: WrappedButtonProps) { + const { add, count, text } = props; + return createElement( + 'button', + { + onClick: add, + }, + createDomTextVNode(text), + createDomTextVNode(': '), + createDomTextVNode(count) + ); +} + +const connector = connect( + state => ({ count: state }), + dispatch => ({ + add: (): void => { + dispatch({ type: 'add' }); + }, + }), + (stateProps, dispatchProps, ownProps: { text: string }) => ({ + add: dispatchProps.add, + count: stateProps.count, + text: ownProps.text, + }) +); + +const ConnectedButton = connector(Button); + +function App() { + return createElement('div', {}, createElement(ConnectedButton, { text: 'click' })); +} + +export default App; diff --git a/packages/inula-reactive/scripts/__tests__/HorizonXTest/class/ClassException.test.tsx b/packages/inula-reactive/scripts/__tests__/HorizonXTest/class/ClassException.test.tsx new file mode 100644 index 00000000..eb198e0d --- /dev/null +++ b/packages/inula-reactive/scripts/__tests__/HorizonXTest/class/ClassException.test.tsx @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import * as Inula from '../../../../src/index'; +import * as LogUtils from '../../jest/logUtils'; +import { clearStore, createStore, useStore } from '../../../../src/inulax/store/StoreHandler'; +import { Text, triggerClickEvent } from '../../jest/commonComponents'; +import { getObserver } from '../../../../src/inulax/proxy/ProxyHandler'; +import { describe, beforeEach, afterEach, it, expect } from '@jest/globals'; + +describe('测试 Class VNode 清除时,对引用清除', () => { + const { unmountComponentAtNode } = Inula; + let container: HTMLElement | null = 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; + LogUtils.clear(); + + clearStore('user'); + }); + + it('test observer.clearByNode', () => { + class Child extends Inula.Component { + userStore = useStore('user'); + + render() { + if (!this.userStore) return
; + // 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(() => { + Inula.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/packages/inula-reactive/scripts/__tests__/HorizonXTest/class/ClassStateArray.test.tsx b/packages/inula-reactive/scripts/__tests__/HorizonXTest/class/ClassStateArray.test.tsx new file mode 100644 index 00000000..7dd050c3 --- /dev/null +++ b/packages/inula-reactive/scripts/__tests__/HorizonXTest/class/ClassStateArray.test.tsx @@ -0,0 +1,248 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import * as Inula from '../../../../src/index'; +import * as LogUtils from '../../jest/logUtils'; +import { clearStore, createStore, useStore } from '../../../../src/inulax/store/StoreHandler'; +import { App, Text, triggerClickEvent } from '../../jest/commonComponents'; +import { describe, beforeEach, afterEach, it, expect } from '@jest/globals'; + +type Person = { name: string; age: number }; + +const persons: Person[] = [ + { name: 'p1', age: 1 }, + { name: 'p2', age: 2 }, +]; +let useUserStore = 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 = []; + }, + }, +}); + +describe('在Class组件中,测试store中的Array', () => { + const { unmountComponentAtNode } = Inula; + let container: HTMLElement | null = null; + beforeEach(() => { + // 创建一个 DOM 元素作为渲染目标 + container = document.createElement('div'); + document.body.appendChild(container); + }); + + afterEach(() => { + // 退出时进行清理 + unmountComponentAtNode(container); + container?.remove(); + container = null; + LogUtils.clear(); + + clearStore('user'); + }); + + const newPerson = { name: 'p3', age: 3 }; + class Parent extends Inula.Component { + userStore = useUserStore(); + props: { + children: any[]; + }; + + constructor(props) { + super(props); + this.props = props; + } + + addOnePerson = () => { + this.userStore.addOnePerson(newPerson); + }; + + delOnePerson = () => { + this.userStore.delOnePerson(); + }; + + render() { + return ( +
+ + +
{this.props.children}
+
+ ); + } + } + + it('测试Array方法: push()、pop()', () => { + class Child extends Inula.Component { + userStore = useUserStore(); + + render() { + return ( +
+ +
+ ); + } + } + + Inula.render(, container); + + expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: 2'); + // 在Array中增加一个对象 + Inula.act(() => { + triggerClickEvent(container, 'addBtn'); + }); + expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: 3'); + + // 在Array中删除一个对象 + Inula.act(() => { + triggerClickEvent(container, 'delBtn'); + }); + expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: 2'); + }); + + it('测试Array方法: entries()、push()、shift()、unshift、直接赋值', () => { + let globalStore = useUserStore(); + + class Child extends Inula.Component { + userStore = useUserStore(); + + constructor(props) { + super(props); + globalStore = this.userStore; + } + + render() { + const nameList: string[] = []; + const entries = this.userStore.$s.persons?.entries(); + if (entries) { + for (const entry of entries) { + nameList.push(entry[1].name); + } + } + + return ( +
+ +
+ ); + } + } + + Inula.render(, container); + + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2'); + // push + globalStore?.$s.persons.push(newPerson); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2 p3'); + + // shift + // @ts-ignore TODO:why has this function argument? + globalStore.$s.persons.shift({ name: 'p0', age: 0 }); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p2 p3'); + + // 赋值[2] + globalStore.$s.persons[2] = { name: 'p4', age: 4 }; + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p2 p3 p4'); + + // 重新赋值[2] + globalStore.$s.persons[2] = { name: 'p5', age: 5 }; + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p2 p3 p5'); + + // unshift + globalStore.$s.persons.unshift({ name: 'p1', age: 1 }); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2 p3 p5'); + + // 重新赋值 [] + globalStore.$s.persons = []; + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: '); + + // 重新赋值 [{ name: 'p1', age: 1 }] + globalStore.$s.persons = [{ name: 'p1', age: 1 }]; + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1'); + }); + + it('测试Array方法: forEach()', () => { + let globalStore = useUserStore(); + globalStore.$s.persons.push({ name: 'p2', age: 2 }); + class Child extends Inula.Component { + userStore = useUserStore(); + + constructor(props) { + super(props); + globalStore = this.userStore; + } + + render() { + const nameList: string[] = []; + this.userStore.$s.persons.forEach((per: Person) => { + nameList.push(per.name); + }); + + return ( +
+ +
+ ); + } + } + + Inula.render(, container); + + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2'); + // push + globalStore.$s.persons.push(newPerson); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2 p3'); + + // shift + // @ts-ignore TODO:why has this function argument? + globalStore.$s.persons.shift({ name: 'p0', age: 0 }); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p2 p3'); + + // 赋值[2] + globalStore.$s.persons[2] = { name: 'p4', age: 4 }; + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p2 p3 p4'); + + // 重新赋值[2] + globalStore.$s.persons[2] = { name: 'p5', age: 5 }; + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p2 p3 p5'); + + // unshift + globalStore.$s.persons.unshift({ name: 'p1', age: 1 }); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2 p3 p5'); + + // 重新赋值 [] + globalStore.$s.persons = []; + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: '); + + // 重新赋值 [{ name: 'p1', age: 1 }] + globalStore.$s.persons = [{ name: 'p1', age: 1 }]; + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1'); + }); +}); diff --git a/packages/inula-reactive/scripts/__tests__/HorizonXTest/class/ClassStateMap.test.tsx b/packages/inula-reactive/scripts/__tests__/HorizonXTest/class/ClassStateMap.test.tsx new file mode 100644 index 00000000..77ec76f2 --- /dev/null +++ b/packages/inula-reactive/scripts/__tests__/HorizonXTest/class/ClassStateMap.test.tsx @@ -0,0 +1,370 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import * as Inula from '../../../../src/index'; +import * as LogUtils from '../../jest/logUtils'; +import { clearStore, createStore, useStore } from '../../../../src/inulax/store/StoreHandler'; +import { App, Text, triggerClickEvent } from '../../jest/commonComponents'; +import { describe, beforeEach, afterEach, it, expect } from '@jest/globals'; + +const useUserStore = createStore({ + id: 'user', + state: { + type: 'bing dun dun', + persons: new Map([ + ['p1', 1], + ['p2', 2], + ]), + }, + actions: { + addOnePerson: (state, person) => { + state.persons.set(person.name, person.age); + }, + delOnePerson: (state, person) => { + state.persons.delete(person.name); + }, + clearPersons: state => { + state.persons.clear(); + }, + reset: state => { + state.persons = new Map([ + ['p1', 1], + ['p2', 2], + ]); + }, + }, +}); + +describe('在Class组件中,测试store中的Map', () => { + const { unmountComponentAtNode } = Inula; + let container: HTMLElement | null = null; + beforeEach(() => { + // 创建一个 DOM 元素作为渲染目标 + container = document.createElement('div'); + document.body.appendChild(container); + + useUserStore().reset(); + }); + + afterEach(() => { + // 退出时进行清理 + unmountComponentAtNode(container); + container?.remove(); + container = null; + LogUtils.clear(); + + clearStore('user'); + }); + + const newPerson = { name: 'p3', age: 3 }; + + class Parent extends Inula.Component { + userStore = useUserStore(); + props = { children: [] }; + + constructor(props) { + super(props); + this.props = props; + } + + 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 Inula.Component { + userStore = useUserStore(); + + render() { + return ( +
+ +
+ ); + } + } + + Inula.render(, container); + + expect(container?.querySelector('#size')?.innerHTML).toBe('persons number: 2'); + // 在Map中增加一个对象 + Inula.act(() => { + triggerClickEvent(container, 'addBtn'); + }); + expect(container?.querySelector('#size')?.innerHTML).toBe('persons number: 3'); + + // 在Map中删除一个对象 + Inula.act(() => { + triggerClickEvent(container, 'delBtn'); + }); + expect(container?.querySelector('#size')?.innerHTML).toBe('persons number: 2'); + + // clear Map + Inula.act(() => { + triggerClickEvent(container, 'clearBtn'); + }); + expect(container?.querySelector('#size')?.innerHTML).toBe('persons number: 0'); + }); + + it('测试Map方法: keys()', () => { + class Child extends Inula.Component { + userStore = useUserStore(); + + render() { + const nameList: string[] = []; + const keys = this.userStore.$s.persons.keys(); + for (const key of keys) { + nameList.push(key); + } + + return ( +
+ +
+ ); + } + } + + Inula.render(, container); + + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2'); + // 在Map中增加一个对象 + Inula.act(() => { + triggerClickEvent(container, 'addBtn'); + }); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2 p3'); + + // 在Map中删除一个对象 + Inula.act(() => { + triggerClickEvent(container, 'delBtn'); + }); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2'); + + // clear Map + Inula.act(() => { + triggerClickEvent(container, 'clearBtn'); + }); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: '); + }); + + it('测试Map方法: values()', () => { + class Child extends Inula.Component { + userStore = useUserStore(); + + render() { + const ageList: number[] = []; + const values = this.userStore.$s.persons.values(); + for (const val of values) { + ageList.push(val); + } + + return ( +
+ +
+ ); + } + } + + Inula.render(, container); + + expect(container?.querySelector('#ageList')?.innerHTML).toBe('age list: 1 2'); + // 在Map中增加一个对象 + Inula.act(() => { + triggerClickEvent(container, 'addBtn'); + }); + expect(container?.querySelector('#ageList')?.innerHTML).toBe('age list: 1 2 3'); + + // 在Map中删除一个对象 + Inula.act(() => { + triggerClickEvent(container, 'delBtn'); + }); + expect(container?.querySelector('#ageList')?.innerHTML).toBe('age list: 1 2'); + + // clear Map + Inula.act(() => { + triggerClickEvent(container, 'clearBtn'); + }); + expect(container?.querySelector('#ageList')?.innerHTML).toBe('age list: '); + }); + + it('测试Map方法: entries()', () => { + class Child extends Inula.Component { + userStore = useUserStore(); + + render() { + const nameList: string[] = []; + const entries = this.userStore.$s.persons.entries(); + for (const entry of entries) { + nameList.push(entry[0]); + } + + return ( +
+ +
+ ); + } + } + + Inula.render(, container); + + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2'); + // 在Map中增加一个对象 + Inula.act(() => { + triggerClickEvent(container, 'addBtn'); + }); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2 p3'); + + // 在Map中删除一个对象 + Inula.act(() => { + triggerClickEvent(container, 'delBtn'); + }); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2'); + + // clear Map + Inula.act(() => { + triggerClickEvent(container, 'clearBtn'); + }); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: '); + }); + + it('测试Map方法: forEach()', () => { + class Child extends Inula.Component { + userStore = useUserStore(); + + render() { + const nameList: string[] = []; + this.userStore.$s.persons.forEach((val, key) => { + nameList.push(key); + }); + + return ( +
+ +
+ ); + } + } + + Inula.render(, container); + + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2'); + // 在Map中增加一个对象 + Inula.act(() => { + triggerClickEvent(container, 'addBtn'); + }); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2 p3'); + + // 在Map中删除一个对象 + Inula.act(() => { + triggerClickEvent(container, 'delBtn'); + }); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2'); + + // clear Map + Inula.act(() => { + triggerClickEvent(container, 'clearBtn'); + }); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: '); + }); + + it('测试Map方法: has()', () => { + class Child extends Inula.Component { + userStore = useUserStore(); + + render() { + return ( +
+ +
+ ); + } + } + + Inula.render(, container); + + expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: false'); + // 在Map中增加一个对象 + Inula.act(() => { + triggerClickEvent(container, 'addBtn'); + }); + expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: true'); + }); + + it('测试Map方法: for of()', () => { + class Child extends Inula.Component { + userStore = useUserStore(); + + render() { + const nameList: string[] = []; + for (const per of this.userStore.$s.persons) { + nameList.push(per[0]); + } + + return ( +
+ +
+ ); + } + } + + Inula.render(, container); + + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2'); + // 在Map中增加一个对象 + Inula.act(() => { + triggerClickEvent(container, 'addBtn'); + }); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2 p3'); + + // 在Map中删除一个对象 + Inula.act(() => { + triggerClickEvent(container, 'delBtn'); + }); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2'); + + // clear Map + Inula.act(() => { + triggerClickEvent(container, 'clearBtn'); + }); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: '); + }); +}); diff --git a/packages/inula-reactive/scripts/__tests__/HorizonXTest/clear/ClassVNodeClear.test.tsx b/packages/inula-reactive/scripts/__tests__/HorizonXTest/clear/ClassVNodeClear.test.tsx new file mode 100644 index 00000000..66a5c1de --- /dev/null +++ b/packages/inula-reactive/scripts/__tests__/HorizonXTest/clear/ClassVNodeClear.test.tsx @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import * as Inula from '../../../../src/index'; +import * as LogUtils from '../../jest/logUtils'; +import { clearStore, createStore, useStore } from '../../../../src/inulax/store/StoreHandler'; +import { Text, triggerClickEvent } from '../../jest/commonComponents'; +import { getObserver } from '../../../../src/inulax/proxy/ProxyHandler'; +import { describe, it, beforeEach, afterEach, expect } from '@jest/globals'; + +describe('测试 Class VNode 清除时,对引用清除', () => { + const { unmountComponentAtNode } = Inula; + let container: HTMLElement | null = 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; + LogUtils.clear(); + + clearStore('user'); + }); + + it('test observer.clearByNode', () => { + class App extends Inula.Component { + userStore = useStore('user'); + + render() { + return ( +
+ + {this.userStore?.isShow && } +
+ ); + } + } + + class Parent extends Inula.Component { + userStore = useStore('user'); + + setWin = () => { + this.userStore?.setWin(!this.userStore?.isWin); + }; + + render() { + return ( +
+ + {this.userStore?.isWin && } +
+ ); + } + } + + class Child extends Inula.Component { + userStore = useStore('user'); + + render() { + // this.userStore.updateName(this.userStore.name === 'bing dun dun' ? 'huo dun dun' : 'bing dun dun'); + + return ( +
+ + +
+ ); + } + } + + Inula.render(, container); + + // Parent and Child hold the isWin key + expect(getObserver(globalState).keyVNodes.get('isWin').size).toBe(2); + + Inula.act(() => { + triggerClickEvent(container, 'toggleBtn'); + }); + // Parent hold the isWin key + expect(getObserver(globalState).keyVNodes.get('isWin').size).toBe(1); + + Inula.act(() => { + triggerClickEvent(container, 'toggleBtn'); + }); + // Parent and Child hold the isWin key + expect(getObserver(globalState).keyVNodes.get('isWin').size).toBe(2); + + Inula.act(() => { + triggerClickEvent(container, 'hideBtn'); + }); + // no component hold the isWin key + expect(getObserver(globalState).keyVNodes.get('isWin')).toBe(undefined); + }); +}); diff --git a/packages/inula-reactive/scripts/__tests__/HorizonXTest/clear/FunctionVNodeClear.test.tsx b/packages/inula-reactive/scripts/__tests__/HorizonXTest/clear/FunctionVNodeClear.test.tsx new file mode 100644 index 00000000..e5ea1d35 --- /dev/null +++ b/packages/inula-reactive/scripts/__tests__/HorizonXTest/clear/FunctionVNodeClear.test.tsx @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import * as Inula from '../../../../src/index'; +import * as LogUtils from '../../jest/logUtils'; +import { clearStore, createStore, useStore } from '../../../../src/inulax/store/StoreHandler'; +import { Text, triggerClickEvent } from '../../jest/commonComponents'; +import { getObserver } from '../../../../src/inulax/proxy/ProxyHandler'; +import { describe, it, beforeEach, afterEach, expect } from '@jest/globals'; + +describe('测试VNode清除时,对引用清除', () => { + const { unmountComponentAtNode } = Inula; + let container: HTMLElement | null = 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; + LogUtils.clear(); + + clearStore('user'); + }); + + it('test observer.clearByNode', () => { + class App extends Inula.Component { + userStore = useStore('user'); + + render() { + return ( +
+ + {this.userStore?.isShow && } +
+ ); + } + } + + class Parent extends Inula.Component { + userStore = useStore('user'); + + setWin = () => { + this.userStore?.setWin(!this.userStore.isWin); + }; + + render() { + return ( +
+ + {this.userStore?.isWin && } +
+ ); + } + } + + class Child extends Inula.Component { + userStore = useStore('user'); + + render() { + return ( +
+ + +
+ ); + } + } + + Inula.render(, container); + + // Parent and Child hold the isWin key + expect(getObserver(globalState).keyVNodes.get('isWin').size).toBe(2); + + Inula.act(() => { + triggerClickEvent(container, 'toggleBtn'); + }); + // Parent hold the isWin key + expect(getObserver(globalState).keyVNodes.get('isWin').size).toBe(1); + + Inula.act(() => { + triggerClickEvent(container, 'toggleBtn'); + }); + // Parent and Child hold the isWin key + expect(getObserver(globalState).keyVNodes.get('isWin').size).toBe(2); + + Inula.act(() => { + triggerClickEvent(container, 'hideBtn'); + }); + // no component hold the isWin key + expect(getObserver(globalState).keyVNodes.get('isWin')).toBe(undefined); + }); +}); diff --git a/packages/inula-reactive/scripts/__tests__/HorizonXTest/edgeCases/deepVariableObserver.test.tsx b/packages/inula-reactive/scripts/__tests__/HorizonXTest/edgeCases/deepVariableObserver.test.tsx new file mode 100644 index 00000000..3853adaa --- /dev/null +++ b/packages/inula-reactive/scripts/__tests__/HorizonXTest/edgeCases/deepVariableObserver.test.tsx @@ -0,0 +1,170 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { createStore, useStore } from '../../../../src/index'; +import { describe, beforeEach, afterEach, it, expect } from '@jest/globals'; + +describe('Using deep variables', () => { + it('should listen to object variable change', () => { + let counter = 0; + const useTestStore = createStore({ + state: { a: { b: { c: 1 } } }, + }); + const testStore = useTestStore(); + testStore.$subscribe(() => { + counter++; + }); + + testStore.a.b.c = 0; + + expect(counter).toBe(1); + }); + + it('should listen to deep variable change', () => { + let counter = 0; + const useTestStore = createStore({ + state: { color: [{ a: 1 }, 255, 255] }, + }); + const testStore = useTestStore(); + testStore.$subscribe(() => { + counter++; + }); + + for (let i = 0; i < 5; i++) { + testStore.color[0].a = i; + } + testStore.color = 'x'; + + expect(counter).toBe(6); + }); + + it('should use set', () => { + const useTestStore = createStore({ + state: { data: new Set() }, + }); + const testStore = useTestStore(); + + const a = { a: true }; + + testStore.data.add(a); + + expect(testStore.data.has(a)).toBe(true); + + testStore.data.add(a); + testStore.data.add(a); + testStore.data.delete(a); + + expect(testStore.data.has(a)).toBe(false); + + testStore.data.add(a); + + const values = Array.from(testStore.data.values()); + expect(values.length).toBe(1); + + let counter = 0; + testStore.$subscribe(mutation => { + counter++; + }); + + values.forEach(val => { + val.a = !val.a; + }); + + expect(testStore.data.has(a)).toBe(true); + + expect(counter).toBe(1); + }); + + it('should use map', () => { + const useTestStore = createStore({ + state: { data: new Map() }, + }); + const testStore = useTestStore(); + + const data = { key: { a: 1 }, value: { b: 2 } }; + + testStore.data.set(data.key, data.value); + + const key = Array.from(testStore.data.keys())[0]; + + expect(testStore.data.has(key)).toBe(true); + + testStore.data.set(data.key, data.value); + testStore.data.set(data.key, data.value); + testStore.data.delete(key); + + expect(testStore.data.get(key)).toBe(); + + testStore.data.set(data.key, data.value); + + const entries = Array.from(testStore.data.entries()); + expect(entries.length).toBe(1); + + let counter = 0; + testStore.$subscribe(mutation => { + counter++; + }); + + entries.forEach(([key, value]) => { + key.a++; + value.b++; + }); + + expect(counter).toBe(2); + }); + + it('should use weakSet', () => { + const useTestStore = createStore({ + state: { data: new WeakSet() }, + }); + const testStore = useTestStore(); + + const a = { a: true }; + + testStore.data.add(a); + + expect(testStore.data.has(a)).toBe(true); + + testStore.data.add(a); + testStore.data.add(a); + testStore.data.delete(a); + + expect(testStore.data.has(a)).toBe(false); + + testStore.data.add(a); + + expect(testStore.data.has(a)).toBe(true); + }); + + it('should use weakMap', () => { + const useTestStore = createStore({ + state: { data: new WeakMap() }, + }); + const testStore = useTestStore(); + + const data = { key: { a: 1 }, value: { b: 2 } }; + + testStore.data.set(data.key, data.value); + + let counter = 0; + testStore.$subscribe(mutation => { + counter++; + }); + + testStore.data.get(data.key).b++; + + expect(counter).toBe(1); + }); +}); diff --git a/packages/inula-reactive/scripts/__tests__/HorizonXTest/edgeCases/multipleStores.test.tsx b/packages/inula-reactive/scripts/__tests__/HorizonXTest/edgeCases/multipleStores.test.tsx new file mode 100644 index 00000000..17ec7b19 --- /dev/null +++ b/packages/inula-reactive/scripts/__tests__/HorizonXTest/edgeCases/multipleStores.test.tsx @@ -0,0 +1,212 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +//@ts-ignore +import Inula, { createStore } from '../../../../src/index'; +import { triggerClickEvent } from '../../jest/commonComponents'; +import { describe, beforeEach, afterEach, it, expect } from '@jest/globals'; + +const { unmountComponentAtNode } = Inula; + +const useStore1 = createStore({ + 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), + }, +}); + +describe('Using multiple stores', () => { + let container: HTMLElement | null = null; + + const BUTTON_ID = 'btn'; + const BUTTON_ID2 = 'btn2'; + const RESULT_ID = 'result'; + + beforeEach(() => { + container = document.createElement('div'); + document.body.appendChild(container); + useStore1().reset(); + useStore2().reset(); + }); + + afterEach(() => { + unmountComponentAtNode(container); + container?.remove(); + container = null; + }); + + it('Should use multiple stores in class component', () => { + class App extends Inula.Component { + render() { + const { counter, add } = useStore1(); + const { counter2, add2 } = useStore2(); + + return ( +
+ + +

+ {counter} {counter2} +

+
+ ); + } + } + + Inula.render(, container); + + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('1 1'); + Inula.act(() => { + triggerClickEvent(container, BUTTON_ID); + }); + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('2 1'); + + Inula.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; + } + class App extends Inula.Component { + constructor() { + super(); + this.store = useStore1(); + this.store2 = useStore2(); + } + + render() { + const { counter, add } = useStore1(); + const store2 = useStore2(); + const { counter2, add2 } = store2; + + for (let i = 0; i < 100; i++) { + const { counter, add } = useStore1(); + const store2 = useStore2(); + const { counter2, add2 } = store2; + } + + return ( +
+ + +

+ {counter} {counter2} +

+
+ ); + } + } + + Inula.render(, container); + + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('1 1'); + Inula.act(() => { + triggerClickEvent(container, BUTTON_ID); + }); + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('2 1'); + + Inula.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 store2 = useStore2(); + const { counter2, add2 } = store2; + + return ( +
+ + +

+ {counter} {counter2} +

+
+ ); + } + + Inula.render(, container); + + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('1 1'); + Inula.act(() => { + triggerClickEvent(container, BUTTON_ID); + }); + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('2 1'); + + Inula.act(() => { + triggerClickEvent(container, BUTTON_ID2); + }); + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('2 2'); + }); +}); diff --git a/packages/inula-reactive/scripts/__tests__/HorizonXTest/edgeCases/proxy.test.tsx b/packages/inula-reactive/scripts/__tests__/HorizonXTest/edgeCases/proxy.test.tsx new file mode 100644 index 00000000..43c12d82 --- /dev/null +++ b/packages/inula-reactive/scripts/__tests__/HorizonXTest/edgeCases/proxy.test.tsx @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { createProxy } from '../../../../src/inulax/proxy/ProxyHandler'; +import { readonlyProxy } from '../../../../src/inulax/proxy/readonlyProxy'; +import { describe, beforeEach, afterEach, it, expect } from '@jest/globals'; + +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); + }); + + it('Readonly proxy should prevent changes', async () => { + const proxy1 = readonlyProxy([1]); + + try { + proxy1.push('a'); + expect(true).toBe(false); //we expect exception above + } catch (e) { + //expected + } + + try { + proxy1[0] = null; + expect(true).toBe(false); //we expect exception above + } catch (e) { + //expected + } + + try { + delete proxy1[0]; + expect(true).toBe(false); //we expect exception above + } catch (e) { + //expected + } + }); +}); diff --git a/packages/inula-reactive/scripts/__tests__/InulaIsTest/index.test.js b/packages/inula-reactive/scripts/__tests__/InulaIsTest/index.test.js new file mode 100644 index 00000000..e6d94933 --- /dev/null +++ b/packages/inula-reactive/scripts/__tests__/InulaIsTest/index.test.js @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import * as Inula from '../../../src/index'; + +function App() { + return <>; +} + +describe('InulaIs', () => { + it('should identify inula elements', () => { + expect(Inula.isElement(
)).toBe(true); + expect(Inula.isElement('span')).toBe(false); + expect(Inula.isElement(111)).toBe(false); + expect(Inula.isElement(false)).toBe(false); + expect(Inula.isElement(null)).toBe(false); + expect(Inula.isElement([])).toBe(false); + expect(Inula.isElement({})).toBe(false); + expect(Inula.isElement(undefined)).toBe(false); + + const TestContext = Inula.createContext(false); + expect(Inula.isElement()).toBe(true); + expect(Inula.isElement()).toBe(true); + expect(Inula.isElement(<>)).toBe(true); + expect(Inula.isElement()).toBe(true); + }); + + it('should identify Fragment', () => { + expect(Inula.isFragment(<>)).toBe(true); + }); + + it('should identify memo component', () => { + const MemoComp = Inula.memo(App); + expect(Inula.isMemo()).toBe(true); + }); + + it('should identify forwardRef', () => { + const ForwardRefComp = Inula.forwardRef(App); + expect(Inula.isForwardRef()).toBe(true); + }); + + it('should identify lazy', () => { + const LazyComp = Inula.lazy(() => App); + expect(Inula.isLazy()).toBe(true); + }); + + it('should identify portal', () => { + const portal = Inula.createPortal(
, container); + expect(Inula.isPortal(portal)).toBe(true); + }); + + it('should identify ContextProvider', () => { + const TestContext = Inula.createContext(false); + expect(Inula.isContextProvider()).toBe(true); + expect(Inula.isContextProvider()).toBe(false); + expect(Inula.isContextConsumer()).toBe(false); + expect(Inula.isContextConsumer()).toBe(true); + }); +}); diff --git a/packages/inula-reactive/scripts/__tests__/ReactivityTest/component/For/reactive-component-for-add.test.js b/packages/inula-reactive/scripts/__tests__/ReactivityTest/component/For/reactive-component-for-add.test.js new file mode 100644 index 00000000..f6a91f8b --- /dev/null +++ b/packages/inula-reactive/scripts/__tests__/ReactivityTest/component/For/reactive-component-for-add.test.js @@ -0,0 +1,162 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import Inula, { render, createRef, useReactive, For } from '../../../../../src/index'; +import { beforeEach } from '@jest/globals'; + +const Row = ({ item }) => { + return
  • {item.name}
  • ; +}; + +let rObj; +let ref; +let appFn; +let App; +let itemFn; + +describe('测试 For 组件的新增', () => { + beforeEach(() => { + ref = createRef(); + appFn = jest.fn(); + itemFn = jest.fn(); + + App = () => { + const _rObj = useReactive({ + items: [ + { id: 'id-1', name: 'p1' }, + { id: 'id-2', name: 'p2' }, + ], + }); + rObj = _rObj; + + appFn(); + + return ( +
    + + {item => { + itemFn(); + return ; + }} + +
    + ); + }; + }); + + it('通过 push 在后面添加1行', () => { + render(, container); + let items = container.querySelectorAll('li'); + expect(items.length).toEqual(2); + + // 在后面添加一行 + rObj.items.push({ id: 'id-3', name: 'p3' }); + + items = container.querySelectorAll('li'); + expect(items.length).toEqual(3); + expect(appFn).toHaveBeenCalledTimes(1); + + // 第一次渲染执行2次,push更新执行1次 + expect(itemFn).toHaveBeenCalledTimes(3); + }); + + it('通过 unshift 在前面添加2行', () => { + render(, container); + let items = container.querySelectorAll('li'); + expect(items.length).toEqual(2); + + // 在前面添加2行 + rObj.items.unshift({ id: 'id-3', name: 'p3' }, { id: 'id-4', name: 'p4' }); + + items = container.querySelectorAll('li'); + expect(items.length).toEqual(4); + expect(appFn).toHaveBeenCalledTimes(1); + + // 第一次渲染执行2次,unshift更新执行2次 + expect(itemFn).toHaveBeenCalledTimes(4); + }); + + it('通过 set 在后面添加1行', () => { + render(, container); + let items = container.querySelectorAll('li'); + expect(items.length).toEqual(2); + + // 在后面添加一行 + rObj.items.set([ + { id: 'id-1', name: 'p1' }, + { id: 'id-2', name: 'p2' }, + { id: 'id-3', name: 'p3' }, + ]); + + items = container.querySelectorAll('li'); + expect(items.length).toEqual(3); + expect(appFn).toHaveBeenCalledTimes(1); + + // 第一次渲染执行2次,push更新执行1次 + expect(itemFn).toHaveBeenCalledTimes(3); + + let li = container.querySelector('#id-3'); + expect(li.innerHTML).toEqual('p3'); + }); + + it('For标签使用,使用push创建3000行表格数据', () => { + let reactiveObj; + const App = () => { + const sourceData = useReactive([]); + reactiveObj = sourceData; + + return ( +
    + + + + + + + + + + + { + eachItem => { + return ( + + + + + + + + + ); + } + } + +
    序号名称年龄性别名族其他
    {eachItem.value}{eachItem.value}{eachItem.value}{eachItem.value}{eachItem.value}{eachItem.value}
    +
    + ); + }; + render(, container); + + // 不推荐:循环push + for (let i = 0; i < 2; i++) { + reactiveObj.push({ value: i, color: null }); + } + expect(reactiveObj.get().length).toEqual(2); + + let items = container.querySelectorAll('tr'); + expect(items.length).toEqual(3); + }); +}); diff --git a/packages/inula-reactive/scripts/__tests__/ReactivityTest/component/For/reactive-component-for-delete.test.js b/packages/inula-reactive/scripts/__tests__/ReactivityTest/component/For/reactive-component-for-delete.test.js new file mode 100644 index 00000000..b53be008 --- /dev/null +++ b/packages/inula-reactive/scripts/__tests__/ReactivityTest/component/For/reactive-component-for-delete.test.js @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import Inula, { render, createRef, useReactive, For } from '../../../../../src/index'; +import { beforeEach } from '@jest/globals'; + +const Row = ({ item }) => { + return
  • {item.name}
  • ; +}; + +let rObj; +let ref; +let appFn; +let App; +let itemFn; + +describe('测试 For 组件的删除', () => { + beforeEach(() => { + ref = createRef(); + appFn = jest.fn(); + itemFn = jest.fn(); + + App = () => { + const _rObj = useReactive({ + items: [ + { id: 'id-1', name: 'p1' }, + { id: 'id-2', name: 'p2' }, + { id: 'id-3', name: 'p3' }, + { id: 'id-4', name: 'p4' }, + { id: 'id-5', name: 'p5' }, + ], + }); + rObj = _rObj; + + appFn(); + + return ( +
    + + {item => { + itemFn(); + return ; + }} + +
    + ); + }; + }); + + it('通过 pop 删除最后1行', () => { + render(, container); + let items = container.querySelectorAll('li'); + expect(items.length).toEqual(5); + + // 删除最后一行 + rObj.items.pop(); + + items = container.querySelectorAll('li'); + expect(items.length).toEqual(4); + expect(appFn).toHaveBeenCalledTimes(1); + + // 第一次渲染执行5次,pop无需更新 + expect(itemFn).toHaveBeenCalledTimes(5); + }); + + it('通过 splice 删除中间2行', () => { + render(, container); + let items = container.querySelectorAll('li'); + expect(items.length).toEqual(5); + + // 删除中间一行 + rObj.items.splice(2, 2); + + items = container.querySelectorAll('li'); + expect(items.length).toEqual(3); + expect(appFn).toHaveBeenCalledTimes(1); + + // 第一次渲染执行5次,splice无需更新 + expect(itemFn).toHaveBeenCalledTimes(5); + }); + + it('通过 splice 删除中间2行,增加1行', () => { + render(, container); + let items = container.querySelectorAll('li'); + expect(items.length).toEqual(5); + + // 删除中间2行,增加1行 + rObj.items.splice(2, 2, ...[{ id: 6, name: 'p6' }]); + + items = container.querySelectorAll('li'); + expect(items.length).toEqual(4); + expect(appFn).toHaveBeenCalledTimes(1); + + // 第一次渲染执行5次,splice新增1行会执行1次 + expect(itemFn).toHaveBeenCalledTimes(6); + }); + + it('通过 set 删除中间2行', () => { + render(, container); + let items = container.querySelectorAll('li'); + expect(items.length).toEqual(5); + + // 删除中间2行 + rObj.items.set([ + { id: 'id-1', name: 'p1' }, + { id: 'id-2', name: 'p2' }, + { id: 'id-5', name: 'p5' }, + ]); + + items = container.querySelectorAll('li'); + expect(items.length).toEqual(3); + expect(appFn).toHaveBeenCalledTimes(1); + + // 第一次渲染执行5次,splice无需更新 + expect(itemFn).toHaveBeenCalledTimes(5); + }); +}); diff --git a/packages/inula-reactive/scripts/__tests__/ReactivityTest/component/For/reactive-component-for-update.test.js b/packages/inula-reactive/scripts/__tests__/ReactivityTest/component/For/reactive-component-for-update.test.js new file mode 100644 index 00000000..272c7f12 --- /dev/null +++ b/packages/inula-reactive/scripts/__tests__/ReactivityTest/component/For/reactive-component-for-update.test.js @@ -0,0 +1,1172 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import Inula, { render, createRef, useReactive, reactive, For } from '../../../../../src/index'; +import { beforeEach } from '@jest/globals'; +import { getRNode } from '../../../../../src/reactive/Utils'; + +const Row = ({ item }) => { + return ( +
  • + {item.name} +
  • + ); +}; + +const TableList = ({ item }) => { + return {item => }; +}; + +const RowList = ({ item }) => { + return {item => }; +}; + +let rObj; +let ref; +let appFn; +let App; +let itemFn; +let globalData; + +describe('测试 For 组件的更新', () => { + beforeEach(() => { + ref = createRef(); + appFn = jest.fn(); + itemFn = jest.fn(); + + App = () => { + const _rObj = useReactive({ + items: [ + { id: 'id-1', name: 'p1' }, + { id: 'id-2', name: 'p2' }, + { id: 'id-3', name: 'p3' }, + { id: 'id-4', name: 'p4' }, + { id: 'id-5', name: 'p5' }, + ], + }); + rObj = _rObj; + + appFn(); + + return ( +
    + + {item => { + itemFn(); + return ; + }} + +
    + ); + }; + }); + + it('通过 set 更新每行数据的id', () => { + render(, container); + let items = container.querySelectorAll('li'); + expect(items.length).toEqual(5); + + let a = container.querySelector('#id-1'); + expect(a.innerHTML).toEqual('p1'); + + // 更新id + rObj.items.set([ + { id: 'id-11', name: 'p1' }, + { id: 'id-22', name: 'p2' }, + { id: 'id-33', name: 'p3' }, + { id: 'id-44', name: 'p4' }, + { id: 'id-55', name: 'p5' }, + ]); + + items = container.querySelectorAll('li'); + expect(items.length).toEqual(5); + expect(appFn).toHaveBeenCalledTimes(1); + + // 只有第一次渲染执行5次 + expect(itemFn).toHaveBeenCalledTimes(5); + + a = container.querySelector('#id-11'); + expect(a.innerHTML).toEqual('p1'); + }); + + it('等长 set 更新每行数据', () => { + render(, container); + let items = container.querySelectorAll('li'); + expect(items.length).toEqual(5); + + let li = container.querySelector('#id-1'); + expect(li.innerHTML).toEqual('p1'); + + // 更新 + rObj.items.set([ + { id: 'id-1', name: 'p11' }, + { id: 'id-2', name: 'p2' }, + { id: 'id-3', name: 'p33' }, + { id: 'id-4', name: 'p4' }, + { id: 'id-5', name: 'p55' }, + ]); + + items = container.querySelectorAll('li'); + expect(items.length).toEqual(5); + expect(appFn).toHaveBeenCalledTimes(1); + + // 只有第一次渲染执行5次 + expect(itemFn).toHaveBeenCalledTimes(5); + + li = container.querySelector('#id-1'); + expect(li.innerHTML).toEqual('p11'); + }); + + it('通过 reverse 反转数组', () => { + render(, container); + let items = container.querySelectorAll('li'); + expect(items.length).toEqual(5); + + // 反转数组 + rObj.items.reverse(); + + items = container.querySelectorAll('li'); + expect(items.length).toEqual(5); + expect(appFn).toHaveBeenCalledTimes(1); + + // 第一次渲染执行5次,反转需要5-1次 + expect(itemFn).toHaveBeenCalledTimes(9); + + let li1 = container.querySelector('li:nth-child(1)'); + expect(li1.innerHTML).toEqual('p5'); + let li2 = container.querySelector('li:nth-child(2)'); + expect(li2.innerHTML).toEqual('p4'); + let li3 = container.querySelector('li:nth-child(3)'); + expect(li3.innerHTML).toEqual('p3'); + let li4 = container.querySelector('li:nth-child(4)'); + expect(li4.innerHTML).toEqual('p2'); + let li5 = container.querySelector('li:nth-child(5)'); + expect(li5.innerHTML).toEqual('p1'); + }); + + it('通过 copyWithin 修改数组', () => { + render(, container); + let items = container.querySelectorAll('li'); + expect(items.length).toEqual(5); + + // 反转数组 + rObj.items.copyWithin(3, 1, 4); + + items = container.querySelectorAll('li'); + expect(items.length).toEqual(5); + expect(appFn).toHaveBeenCalledTimes(1); + + expect(itemFn).toHaveBeenCalledTimes(7); + + // 结果是: + // { id: 'id-1', name: 'p1' }, + // { id: 'id-2', name: 'p2' }, + // { id: 'id-3', name: 'p3' }, + // { id: 'id-2', name: 'p2' }, + // { id: 'id-3', name: 'p3' }, + + let li1 = container.querySelector('li:nth-child(1)'); + expect(li1.innerHTML).toEqual('p1'); + let li2 = container.querySelector('li:nth-child(2)'); + expect(li2.innerHTML).toEqual('p2'); + let li3 = container.querySelector('li:nth-child(3)'); + expect(li3.innerHTML).toEqual('p3'); + let li4 = container.querySelector('li:nth-child(4)'); + expect(li4.innerHTML).toEqual('p2'); + let li5 = container.querySelector('li:nth-child(5)'); + expect(li5.innerHTML).toEqual('p3'); + }); +}); + +describe('测试 For 组件的更新,3层数据', () => { + beforeEach(() => { + ref = createRef(); + appFn = jest.fn(); + itemFn = jest.fn(); + + App = () => { + const _rObj = useReactive({ + items: [ + { + id: 'id-1', + items: [ + { + id: 'id-1', + items: [ + { id: 'id-1', name: 'p1' }, + { id: 'id-2', name: 'p2' }, + { id: 'id-3', name: 'p3' }, + ], + }, + { + id: 'id-2', + items: [ + { id: 'id-4', name: 'p4' }, + { id: 'id-5', name: 'p5' }, + { id: 'id-6', name: 'p6' }, + ], + }, + { + id: 'id-3', + items: [ + { id: 'id-7', name: 'p7' }, + { id: 'id-8', name: 'p8' }, + { id: 'id-9', name: 'p9' }, + ], + }, + ], + }, + { + id: 'id-2', + items: [ + { + id: 'id-1', + items: [ + { id: 'id-10', name: 'p10' }, + { id: 'id-11', name: 'p11' }, + { id: 'id-12', name: 'p12' }, + ], + }, + { + id: 'id-2', + items: [ + { id: 'id-13', name: 'p13' }, + { id: 'id-14', name: 'p14' }, + { id: 'id-15', name: 'p15' }, + ], + }, + { + id: 'id-3', + items: [ + { id: 'id-16', name: 'p16' }, + { id: 'id-17', name: 'p17' }, + { id: 'id-18', name: 'p18' }, + ], + }, + ], + }, + ], + }); + rObj = _rObj; + + appFn(); + + return ( +
    + + {item => { + itemFn(); + return ; + }} + +
    + ); + }; + }); + + it('通过 set 更新第三层数据', () => { + render(, container); + let items = container.querySelectorAll('li'); + expect(items.length).toEqual(18); + + let li = container.querySelector('#id-4'); + expect(li.innerHTML).toEqual('p4'); + + // 更新 + rObj.items.set([ + { + id: 'id-1', + items: [ + { + id: 'id-1', + items: [ + { id: 'id-1', name: 'p1' }, + { id: 'id-2', name: 'p2' }, + { id: 'id-3', name: 'p3' }, + ], + }, + { + id: 'id-2', + items: [ + { id: 'id-4', name: 'p444' }, + { id: 'id-5', name: 'p5' }, + { id: 'id-6', name: 'p6' }, + ], + }, + { + id: 'id-3', + items: [ + { id: 'id-7', name: 'p7' }, + { id: 'id-8', name: 'p8' }, + { id: 'id-9', name: 'p9' }, + ], + }, + ], + }, + { + id: 'id-2', + items: [ + { + id: 'id-1', + items: [ + { id: 'id-10', name: 'p10' }, + { id: 'id-11', name: 'p11' }, + { id: 'id-12', name: 'p12' }, + ], + }, + { + id: 'id-2', + items: [ + { id: 'id-13', name: 'p13' }, + { id: 'id-14', name: 'p14' }, + { id: 'id-15', name: 'p15' }, + ], + }, + { + id: 'id-3', + items: [ + { id: 'id-16', name: 'p16' }, + { id: 'id-17', name: 'p17' }, + { id: 'id-18', name: 'p18' }, + ], + }, + ], + }, + ]); + + items = container.querySelectorAll('li'); + expect(items.length).toEqual(18); + expect(appFn).toHaveBeenCalledTimes(1); + + // 只有第一次渲染执行2次 + expect(itemFn).toHaveBeenCalledTimes(2); + + li = container.querySelector('#id-4'); + expect(li.innerHTML).toEqual('p444'); + }); + + it('通过 set 删除第3层数据', () => { + render(, container); + let items = container.querySelectorAll('li'); + expect(items.length).toEqual(18); + + let li = container.querySelector('#id-8'); + expect(li.innerHTML).toEqual('p8'); + + // 更新 + rObj.items.set([ + { + id: 'id-1', + items: [ + { + id: 'id-1', + items: [{ id: 'id-1', name: 'p1' }], + }, + { + id: 'id-2', + items: [ + { id: 'id-4', name: 'p4' }, + { id: 'id-5', name: 'p5' }, + ], + }, + { + id: 'id-3', + items: [ + { id: 'id-7', name: 'p7' }, + { id: 'id-8', name: 'p888' }, + { id: 'id-9', name: 'p9' }, + ], + }, + ], + }, + { + id: 'id-2', + items: [ + { + id: 'id-1', + items: [ + { id: 'id-10', name: 'p10' }, + { id: 'id-11', name: 'p11' }, + { id: 'id-12', name: 'p12' }, + ], + }, + { + id: 'id-2', + items: [ + { id: 'id-13', name: 'p13' }, + { id: 'id-14', name: 'p14' }, + { id: 'id-15', name: 'p15' }, + ], + }, + { + id: 'id-3', + items: [ + { id: 'id-16', name: 'p16' }, + { id: 'id-17', name: 'p17' }, + { id: 'id-18', name: 'p18' }, + ], + }, + ], + }, + ]); + + items = container.querySelectorAll('li'); + expect(items.length).toEqual(15); + expect(appFn).toHaveBeenCalledTimes(1); + + // 第一次渲染执行2次,更新也触发了1次 + expect(itemFn).toHaveBeenCalledTimes(3); + + li = container.querySelector('#id-8'); + expect(li.innerHTML).toEqual('p888'); + }); + + it('通过 set 删除第1、3层数据', () => { + render(, container); + let items = container.querySelectorAll('li'); + expect(items.length).toEqual(18); + + let li = container.querySelector('#id-8'); + expect(li.innerHTML).toEqual('p8'); + + // 更新 + rObj.items.set([ + { + id: 'id-1', + items: [ + { + id: 'id-1', + items: [{ id: 'id-1', name: 'p1' }], + }, + { + id: 'id-2', + items: [ + { id: 'id-4', name: 'p4' }, + { id: 'id-5', name: 'p5' }, + ], + }, + { + id: 'id-3', + items: [ + { id: 'id-7', name: 'p7' }, + { id: 'id-8', name: 'p888' }, + { id: 'id-9', name: 'p9' }, + ], + }, + ], + }, + ]); + + items = container.querySelectorAll('li'); + expect(items.length).toEqual(6); + expect(appFn).toHaveBeenCalledTimes(1); + + // 第一次渲染执行2次,更新也触发了1次 + expect(itemFn).toHaveBeenCalledTimes(3); + + li = container.querySelector('#id-8'); + expect(li.innerHTML).toEqual('p888'); + }); + + it('通过 set 删除第1、2、3层数据', () => { + render(, container); + let items = container.querySelectorAll('li'); + expect(items.length).toEqual(18); + + let li = container.querySelector('#id-8'); + expect(li.innerHTML).toEqual('p8'); + + // 更新 + rObj.items.set([ + { + id: 'id-1', + items: [ + { + id: 'id-2', + items: [ + { id: 'id-4', name: 'p4' }, + { id: 'id-5', name: 'p5' }, + ], + }, + { + id: 'id-3', + items: [ + { id: 'id-7', name: 'p7' }, + { id: 'id-8', name: 'p888' }, + { id: 'id-9', name: 'p9' }, + ], + }, + ], + }, + ]); + + items = container.querySelectorAll('li'); + expect(items.length).toEqual(5); + expect(appFn).toHaveBeenCalledTimes(1); + + // 第一次渲染执行2次,更新也触发了1次 + expect(itemFn).toHaveBeenCalledTimes(3); + + li = container.querySelector('#id-8'); + expect(li.innerHTML).toEqual('p888'); + }); + + it('通过 set 把数组设置成boolean和number', () => { + render(, container); + let items = container.querySelectorAll('li'); + expect(items.length).toEqual(18); + + // 更新 + rObj.items.set([ + { + id: 'id-1', + items: [ + { + id: 'id-1', + items: [true], + }, + { + id: 'id-2', + items: 11, + }, + { + id: 'id-3', + items: [ + { id: 'id-7', name: 'p7' }, + { id: 'id-8', name: 'p8' }, + { id: 'id-9', name: 'p9' }, + ], + }, + ], + }, + ]); + + items = container.querySelectorAll('li'); + expect(items.length).toEqual(4); + expect(appFn).toHaveBeenCalledTimes(1); + + // 第一次渲染执行2次,更新也触发了1次 + expect(itemFn).toHaveBeenCalledTimes(3); + }); + + it('通过 set 把数组设置成boolean和number,再修改下面数据部分', () => { + render(, container); + let items = container.querySelectorAll('li'); + expect(items.length).toEqual(18); + + // 更新 + rObj.items.set([ + { + id: 'id-1', + items: [ + { + id: 'id-1', + items: [true], + }, + { + id: 'id-2', + items: 11, // 数组变数字,不会报错 + xxx: 'xxx', // 多出来的数据不影响 + }, + { + id: 'id-3', + items: [ + { id: 'id-7', name: 'p7' }, + { id: 'id-8', name: 'p888' }, + { id: 'id-9', name: 'p9' }, + ], + }, + ], + }, + ]); + + items = container.querySelectorAll('li'); + expect(items.length).toEqual(4); + expect(appFn).toHaveBeenCalledTimes(1); + + // 第一次渲染执行2次,更新也触发了1次 + expect(itemFn).toHaveBeenCalledTimes(3); + + let li = container.querySelector('#id-8'); + expect(li.innerHTML).toEqual('p888'); + }); + + it('通过 set 把对象中的数组设置成boolean', () => { + App = () => { + const _rObj = useReactive({ + id: 'id-1', + items: [ + { id: 'id-1', name: 'p1' }, + { id: 'id-2', name: 'p2' }, + { id: 'id-3', name: 'p3' }, + ], + }); + rObj = _rObj; + + appFn(); + + return ( +
    + + {item => { + itemFn(); + return ( +
  • + {item.name} +
  • + ); + }} +
    +
    + ); + }; + + render(, container); + let items = container.querySelectorAll('li'); + expect(items.length).toEqual(3); + + // 更新 + rObj.set({ + id: 'id-1', + items: [true], + }); + + items = container.querySelectorAll('li'); + expect(items.length).toEqual(1); + expect(appFn).toHaveBeenCalledTimes(1); + + // 第一次渲染执行3次,更新也触发了1次 + expect(itemFn).toHaveBeenCalledTimes(4); + }); +}); + +describe('测试 For 组件的更新,反复增删', () => { + beforeEach(() => { + ref = createRef(); + appFn = jest.fn(); + itemFn = jest.fn(); + + App = () => { + const _rObj = useReactive({ + items: [], + }); + rObj = _rObj; + + appFn(); + + return ( +
    + + {item => { + itemFn(); + return ; + }} + +
    + ); + }; + }); + + it('先用 splice 删除1行,再通过 set 新增2行', () => { + render(, container); + + function removeFirstRow() { + rObj.items.splice(0, 1); + } + + removeFirstRow(); + + // 新增2行 + rObj.items.set([ + { id: 'id-1', name: 'p1' }, + { id: 'id-2', name: 'p2' }, + ]); + + let items = container.querySelectorAll('li'); + expect(items.length).toEqual(2); + + // 再新增2行 + rObj.items.set( + rObj.items.concat([ + { id: 'id-3', name: 'p3' }, + { id: 'id-4', name: 'p4' }, + ]) + ); + + items = container.querySelectorAll('li'); + expect(items.length).toEqual(4); + }); + + it('先用 set 新增6行,删除1行,交换两行位置', () => { + render(, container); + + // 新增2行 + rObj.items.set([ + { id: 'id-1', name: 'p1' }, + { id: 'id-2', name: 'p2' }, + { id: 'id-3', name: 'p3' }, + { id: 'id-4', name: 'p4' }, + { id: 'id-5', name: 'p5' }, + { id: 'id-6', name: 'p6' }, + ]); + + let items = container.querySelectorAll('li'); + expect(items.length).toEqual(6); + + // 删除一行 + rObj.items.splice(0, 1); + + items = container.querySelectorAll('li'); + expect(items.length).toEqual(5); + + function swapRows() { + const arr = rObj.items.slice(); + const tmp = arr[1]; + arr[1] = arr[arr.length - 2]; + arr[arr.length - 2] = tmp; + rObj.items.set(arr); + } + + swapRows(); + + items = container.querySelectorAll('li'); + expect(items.length).toEqual(5); + + let li2 = container.querySelector('li:nth-child(2)'); + expect(li2.innerHTML).toEqual('p5'); + + let li4 = container.querySelector('li:nth-child(4)'); + expect(li4.innerHTML).toEqual('p3'); + }); + + it('先用 set 新增4行,交换两行位置,删除1行', () => { + render(, container); + + // 新增2行 + rObj.items.set([ + { id: 'id-1', name: 'p1' }, + { id: 'id-2', name: 'p2' }, + { id: 'id-3', name: 'p3' }, + { id: 'id-4', name: 'p4' }, + ]); + + let items = container.querySelectorAll('li'); + expect(items.length).toEqual(4); + + function swapRows() { + const arr = rObj.items.slice(); + const tmp = arr[1]; + arr[1] = arr[arr.length - 2]; + arr[arr.length - 2] = tmp; + rObj.items.set(arr); + } + + // 前后边上第2行交换 + swapRows(); + + // 删除一行 + rObj.items.splice(0, 1); + + // 结果是: + // { id: 'id-3', name: 'p3' }, + // { id: 'id-2', name: 'p2' }, + // { id: 'id-4', name: 'p4' }, + + let li2 = container.querySelector('li:nth-child(2)'); + expect(li2.innerHTML).toEqual('p2'); + }); +}); + +describe('测试 For 组件的更新,直接修改raw数组对象', () => { + beforeEach(() => { + ref = createRef(); + appFn = jest.fn(); + itemFn = jest.fn(); + + globalData = { + items: [ + { id: 'id-1', name: 'p1' }, + { id: 'id-2', name: 'p2' }, + { id: 'id-3', name: 'p3' }, + { id: 'id-4', name: 'p4' }, + { id: 'id-5', name: 'p5' }, + ], + }; + + App = () => { + const _rObj = useReactive(globalData); + rObj = _rObj; + + appFn(); + + return ( +
    + + {item => { + itemFn(); + return ; + }} + +
    + ); + }; + }); + + it('向原始数组中增加1行数据,再通过 set 更新响应式数据,是不会更新的', () => { + render(, container); + + let items = container.querySelectorAll('li'); + expect(items.length).toEqual(5); + + // 新增1行 + globalData.items.push({ id: 'id-6', name: 'p6' }); + + // 无法触发更新,因为globalData.items的引用相同,不会触发监听 + rObj.set(globalData); + + items = container.querySelectorAll('li'); + // 不会更新 + expect(items.length).toEqual(5); + }); + + it('应该直接修改响应式数据的方式', () => { + render(, container); + + let items = container.querySelectorAll('li'); + expect(items.length).toEqual(5); + + // 直接修改响应式数据的方式,新增1行 + rObj.items.push({ id: 'id-6', name: 'p6' }); + items = container.querySelectorAll('li'); + expect(items.length).toEqual(6); + }); +}); + +describe('更新多属性对象', () => { + beforeEach(() => { + ref = createRef(); + appFn = jest.fn(); + itemFn = jest.fn(); + + globalData = { + items: [ + { id: 'id-1', name: 'p1', class: 'c1' }, + { id: 'id-2', name: 'p2', class: 'c2' }, + { id: 'id-3', name: 'p3', class: 'c3' }, + { id: 'id-4', name: 'p4', class: 'c4' }, + { id: 'id-5', name: 'p5', class: 'c5' }, + ], + }; + + const Row = ({ item }) => { + return ( +
  • + {item.name} +
  • + ); + }; + + App = () => { + const _rObj = useReactive(globalData); + rObj = _rObj; + + appFn(); + + return ( +
    + + {item => { + itemFn(); + return ; + }} + +
    + ); + }; + }); + + it('对象数据的属性类型变化,后面的属性更正常', () => { + render(, container); + + let items = container.querySelectorAll('li'); + expect(items.length).toEqual(5); + + let li = container.querySelector('#id-2'); + expect(li.getAttribute('class')).toEqual('c2'); + + rObj.set({ + items: [ + { id: 'id-1', name: 'p1', class: 'c1' }, + { id: 'id-2', name: [true], class: 'c2222' }, + { id: 'id-3', name: 'p3', class: 'c3' }, + { id: 'id-4', name: 'p4', class: 'c4' }, + { id: 'id-5', name: 'p5', class: 'c5' }, + ], + }); + + items = container.querySelectorAll('li'); + expect(items.length).toEqual(5); + + li = container.querySelector('#id-2'); + expect(li.getAttribute('class')).toEqual('c2222'); + }); +}); + +describe('在class组件中使用for组件', () => { + it('在类中使用reactive数据', () => { + let rObj; + let appInst; + const ref = createRef(); + + class App extends Inula.Component { + constructor(props) { + super(props); + + appInst = this; + + this.state = { + name: 1, + }; + + this._rObj = reactive(1); + rObj = this._rObj; + } + + render() { + return
    {this._rObj}
    ; + } + } + + render(, container); + + expect(ref.current.innerHTML).toEqual('1'); + + // 触发组件重新渲染 + appInst.setState({ name: 2 }); + + rObj.set('2'); + + // rObj只应该有一个依赖 + expect(rObj.usedRContexts.size).toEqual(1); + + expect(ref.current.innerHTML).toEqual('2'); + }); + + it('在类中使用reactive数组数据', () => { + let rObj; + let appInst; + const ref = createRef(); + + class Row extends Inula.Component { + constructor(props) { + super(props); + } + + render() { + const { item } = this.props; + return ( +
  • + {item.name} +
  • + ); + } + } + + class App extends Inula.Component { + constructor(props) { + super(props); + + appInst = this; + + this.state = { + name: 1, + }; + + this._rObj = reactive({ + items: [ + { id: 'id-1', name: 'p1', class: 'c1' }, + { id: 'id-2', name: 'p2', class: 'c2' }, + { id: 'id-3', name: 'p3', class: 'c3' }, + { id: 'id-4', name: 'p4', class: 'c4' }, + { id: 'id-5', name: 'p5', class: 'c5' }, + ], + }); + rObj = this._rObj; + } + + render() { + return ( +
    + + {item => { + return ; + }} + +
    + ); + } + } + + render(, container); + + let items = container.querySelectorAll('li'); + expect(items.length).toEqual(5); + + // 直接修改响应式数据的方式,新增1行 + rObj.items.push({ id: 'id-6', name: 'p6' }); + items = container.querySelectorAll('li'); + expect(items.length).toEqual(6); + + // 触发组件重新渲染 + appInst.setState({ name: 2 }); + + // rObj只应该有一个依赖 + expect(getRNode(rObj.items).usedRContexts.size).toEqual(1); + }); + + describe('更新多属性对象', () => { + beforeEach(() => { + ref = createRef(); + appFn = jest.fn(); + itemFn = jest.fn(); + + globalData = { + items: [ + { id: 'id-1', name: 'p1', class: 'c1' }, + { id: 'id-2', name: 'p2', class: 'c2' }, + ], + }; + + const Row = ({ item }) => { + return ( +
  • + {item.name} +
  • + ); + }; + + App = () => { + const _rObj = useReactive(globalData); + rObj = _rObj; + + appFn(); + + return ( +
    + + {item => { + itemFn(); + return ; + }} + +
    + ); + }; + }); + + it('更新数组的一个原数据,调试subscribeAttr,只被调用一次', () => { + render(, container); + + let items = container.querySelectorAll('li'); + expect(items.length).toEqual(2); + + let li = container.querySelector('#id-2'); + expect(li.getAttribute('class')).toEqual('c2'); + + rObj.set({ + items: [ + { id: 'id-1', name: 'p1', class: 'c1' }, + { id: 'id-2', name: 'p2', class: 'c222' }, + ], + }); + + items = container.querySelectorAll('li'); + expect(items.length).toEqual(2); + + li = container.querySelector('#id-2'); + expect(li.getAttribute('class')).toEqual('c222'); + }); + + it('更新数组的一个原数据', () => { + render(, container); + + let items = container.querySelectorAll('li'); + expect(items.length).toEqual(2); + + let li = container.querySelector('#id-2'); + expect(li.getAttribute('class')).toEqual('c2'); + + rObj.items[1].set({ id: 'id-2', name: 'p2', class: 'c222' }); + + items = container.querySelectorAll('li'); + expect(items.length).toEqual(2); + + li = container.querySelector('#id-2'); + expect(li.getAttribute('class')).toEqual('c222'); + + expect(itemFn).toHaveBeenCalledTimes(2); + }); + + it('For的数组是基本数据,更改其中一个,另外两个能精准更新', () => { + const rowFn = jest.fn(); + + const Row = ({ item, index }) => { + rowFn(); + return
  • {item}
  • ; + }; + + const App = () => { + const _rObj = useReactive({ + id: 'id-1', + items: [{ a: 1 }, 2, 3], + }); + rObj = _rObj; + + appFn(); + + return ( +
    + + {(item, index) => { + itemFn(); + return ; + }} + +
    + ); + }; + + render(, container); + let items = container.querySelectorAll('li'); + expect(items.length).toEqual(3); + + // 更新 + rObj.set({ + id: 'id-1', + items: [2, 3, 4], + }); + + items = container.querySelectorAll('li'); + expect(items.length).toEqual(3); + expect(appFn).toHaveBeenCalledTimes(1); + + // 第一次渲染执行3次,更新也触发了1次 + expect(itemFn).toHaveBeenCalledTimes(4); + + // 第一次渲染执行3次,更新也触发了1次 + expect(rowFn).toHaveBeenCalledTimes(4); + }); + }); +}); diff --git a/packages/inula-reactive/scripts/__tests__/ReactivityTest/component/For/reactive-component-for.test.js b/packages/inula-reactive/scripts/__tests__/ReactivityTest/component/For/reactive-component-for.test.js new file mode 100644 index 00000000..585b60ba --- /dev/null +++ b/packages/inula-reactive/scripts/__tests__/ReactivityTest/component/For/reactive-component-for.test.js @@ -0,0 +1,251 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import Inula, { render, createRef, useReactive, reactive, memo, For } from '../../../../../src/index'; + +const Item = ({ item }) => { + return
  • {item.name}
  • ; +}; + +describe('测试 For 组件', () => { + it('使用For组件遍历reactive“数组”', () => { + let rObj; + const ref = createRef(); + const fn = jest.fn(); + const Item = ({ item }) => { + return
  • {item.name}
  • ; + }; + + const App = () => { + const _rObj = useReactive({ + items: [ + { name: 'p1', id: 1 }, + { name: 'p2', id: 2 }, + ], + }); + rObj = _rObj; + + fn(); + + return ( +
    + + {item => { + return ; + }} + +
    + ); + }; + + render(, container); + let items = container.querySelectorAll('li'); + expect(items.length).toEqual(2); + + // 每次修改items都会触发整个组件刷新 + rObj.items.set([{ name: 'p11', id: 1 }]); + + items = container.querySelectorAll('li'); + expect(items.length).toEqual(1); + expect(fn).toHaveBeenCalledTimes(1); + + // 每次修改items都会触发整个组件刷新 + rObj.items.push({ name: 'p22', id: 2 }); + + items = container.querySelectorAll('li'); + expect(items.length).toEqual(2); + expect(fn).toHaveBeenCalledTimes(1); + }); + + it('reactive“数组”从[]变成有值', () => { + let rObj; + const ref = createRef(); + const fn = jest.fn(); + const Item = ({ item }) => { + return
  • {item.name}
  • ; + }; + + const App = () => { + const _rObj = useReactive({ + items: [], + }); + rObj = _rObj; + + fn(); + + return ( +
    + + {item => { + return ; + }} + +
    + ); + }; + + render(, container); + let items = container.querySelectorAll('li'); + expect(items.length).toEqual(0); + + // 每次修改items都会触发整个组件刷新 + rObj.items.set([{ name: 'p11', id: 1 }]); + + items = container.querySelectorAll('li'); + expect(items.length).toEqual(1); + expect(fn).toHaveBeenCalledTimes(1); + + // 每次修改items都会触发整个组件刷新 + rObj.items.push({ name: 'p22', id: 2 }); + + items = container.querySelectorAll('li'); + expect(items.length).toEqual(2); + expect(fn).toHaveBeenCalledTimes(1); + }); + + it('数组3行变到4行', () => { + const state = reactive({ + data: { + lines: [ + { id: 'id-1', label: '1' }, + { id: 'id-2', label: '2' }, + { id: 'id-3', label: '3' }, + ], + }, + }); + + const Row = memo(({ item }) => { + return ( + + {item.id} + + {item.label} + + + ); + }); + + const RowList = () => { + return {item => }; + }; + + const App = () => { + return ( +
    + + + + +
    +
    + ); + }; + + render(, container); + + let a = container.querySelector('#id-1'); + + expect(a.innerHTML).toEqual('1'); + expect(state.data.lines.length).toEqual(3); + state.data.set({ + lines: [ + { id: 'id-4', label: '4' }, + { id: 'id-5', label: '5' }, + { id: 'id-6', label: '6' }, + { id: 'id-7', label: '7' }, + ], + }); + expect(state.data.lines.length).toEqual(4); + a = container.querySelector('#id-4'); + + expect(a.innerHTML).toEqual('4'); + const b = container.querySelector('#id-6'); + expect(b.innerHTML).toEqual('6'); + }); + + it('使用基本数据数组的loop方法', () => { + let rObj; + const fn = jest.fn(); + + const App = () => { + const _rObj = useReactive({ + items: [1, 2, 3, 4], + }); + rObj = _rObj; + + fn(); + + return ( +
    + {_rObj.items.map(rItem => { + return
  • {rItem}
  • ; + })} +
    + ); + }; + + render(, container); + let items = container.querySelectorAll('li'); + expect(items.length).toEqual(4); + + // 每次修改items都会触发整个组件刷新 + rObj.items.set([1, 2, 3]); + + items = container.querySelectorAll('li'); + expect(items.length).toEqual(3); + expect(fn).toHaveBeenCalledTimes(2); + }); +}); + +describe('数组reverse', () => { + it('调用数组的reverse方法', () => { + let rObj; + const fn = jest.fn(); + + const App = () => { + const _rObj = useReactive({ + items: [ + { id: 1, name: 'p1' }, + { id: 2, name: 'p2' }, + { id: 3, name: 'p3' }, + ], + }); + rObj = _rObj; + + fn(); + + return ( +
    + + {item => { + return ; + }} + +
    + ); + }; + + render(, container); + let items = container.querySelectorAll('li'); + expect(items.length).toEqual(3); + + // 反转 + rObj.items.reverse(); + + items = container.querySelectorAll('li'); + expect(items.length).toEqual(3); + expect(fn).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/inula-reactive/scripts/__tests__/ReactivityTest/component/reactive-component-block.test.js b/packages/inula-reactive/scripts/__tests__/ReactivityTest/component/reactive-component-block.test.js new file mode 100644 index 00000000..fa89e36e --- /dev/null +++ b/packages/inula-reactive/scripts/__tests__/ReactivityTest/component/reactive-component-block.test.js @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import Inula, { render, createRef, act, useReactive } from '../../../../src/index'; +import { Block } from '../../../../src/reactive/components/Block'; + +describe('测试 Block 组件', () => { + it('使用 Block 控制更新范围', () => { + let rObj, rColor; + const ref = createRef(); + const fn = jest.fn(); + const fn1 = jest.fn(); + const App = () => { + const _rObj = useReactive({ count: 0 }); + const _rColor = useReactive('blue'); + rObj = _rObj; + rColor = _rColor; + + fn(); + + return ( +
    + 111 222 + + {() => { + fn1(); + const count = _rObj.count.get(); + return ( + <> +
    Count: {count}
    +
    {_rColor}
    + + ); + }} +
    +
    + ); + }; + + render(, container); + + expect(ref.current.innerHTML).toEqual('111 222
    Count: 0
    blue
    '); + + // 会触发View刷新 + rObj.count.set(1); + expect(fn).toHaveBeenCalledTimes(1); + expect(fn1).toHaveBeenCalledTimes(2); + expect(ref.current.innerHTML).toEqual('111 222
    Count: 1
    blue
    '); + + // 不会触发View刷新 + rColor.set('red'); + expect(fn).toHaveBeenCalledTimes(1); + expect(fn1).toHaveBeenCalledTimes(2); + expect(ref.current.innerHTML).toEqual('111 222
    Count: 1
    red
    '); + }); + + it('使用 Block 包裹一个Atom', () => { + let rObj; + const ref1 = createRef(); + const fn = jest.fn(); + const App = () => { + const _rObj = useReactive('blue'); + rObj = _rObj; + + fn(); + + return ( + // div下面有多个元素,_rObj就需要用RText包裹 +
    + 111 222 + {_rObj} +
    + ); + }; + + render(, container); + expect(ref1.current.innerHTML).toEqual('111 222blue'); + rObj.set('red'); + expect(fn).toHaveBeenCalledTimes(1); + expect(ref1.current.innerHTML).toEqual('111 222red'); + }); +}); diff --git a/packages/inula-reactive/scripts/__tests__/ReactivityTest/component/reactive-component-combination.test.js b/packages/inula-reactive/scripts/__tests__/ReactivityTest/component/reactive-component-combination.test.js new file mode 100644 index 00000000..6871e89f --- /dev/null +++ b/packages/inula-reactive/scripts/__tests__/ReactivityTest/component/reactive-component-combination.test.js @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import Inula, { render, createRef, useReactive, useComputed, For, Show, Switch } from '../../../../src/index'; + +describe('测试Switch、Show、For标签的组合使用时的组件渲染', () => { + it('Show、For标签的组合使用', () => { + const Item = ({ item }) => { + return
  • {item.name}
  • ; + }; + + let reactiveObj; + const ref = createRef(); + const ref1 = createRef(); + const fn = jest.fn(); + + const App = () => { + const dataList = useReactive([]); + reactiveObj = dataList; + + const listLen = useComputed(() => { + return dataList.get().length; + }); + + fn(); + + return ( + <> + dataList.get().length > 0} else={() =>
    }> +
    + {item => } +
    + +
    {listLen}
    + + ); + }; + render(, container); + + let liItems = container.querySelectorAll('li'); + expect(liItems.length).toEqual(0); + + reactiveObj.push({ id: 1, name: '1' }); + expect(reactiveObj.get().length).toEqual(1); + liItems = container.querySelectorAll('li'); + expect(liItems.length).toEqual(1); + + reactiveObj.push({ id: 2, name: '2' }); + expect(reactiveObj.get().length).toEqual(2); + liItems = container.querySelectorAll('li'); + expect(liItems.length).toEqual(2); + + expect(ref1.current.innerHTML).toEqual('2'); + + expect(fn).toHaveBeenCalledTimes(1); + }); + + it('Switch、Show和For标签的组合使用', () => { + const Item = ({ item }) => { + return
  • {item.name}
  • ; + }; + + let reactiveObj; + const ref = createRef(); + const App = () => { + const dataList = useReactive([]); + reactiveObj = dataList; + + return ( + + dataList.get().length === 0}> +
    + + dataList.get().length > 0}> +
    + {item => } +
    +
    + + ); + }; + render(, container); + + let liItems = container.querySelectorAll('li'); + expect(liItems.length).toEqual(0); + + reactiveObj.push({ id: 1, name: '1' }); + expect(reactiveObj.get().length).toEqual(1); + + liItems = container.querySelectorAll('li'); + expect(liItems.length).toEqual(1); + }); +}); diff --git a/packages/inula-reactive/scripts/__tests__/ReactivityTest/component/reactive-component-rtext.test.js b/packages/inula-reactive/scripts/__tests__/ReactivityTest/component/reactive-component-rtext.test.js new file mode 100644 index 00000000..167ef990 --- /dev/null +++ b/packages/inula-reactive/scripts/__tests__/ReactivityTest/component/reactive-component-rtext.test.js @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import Inula, { render, createRef, act, useReactive, useCompute, reactive, RText } from '../../../../src/index'; + +describe('测试 RText 组件', () => { + it('使用RText精准更新', () => { + let rObj; + const ref1 = createRef(); + const fn = jest.fn(); + const App = () => { + const _rObj = useReactive('blue'); + rObj = _rObj; + + fn(); + + return ( + // div下面有多个元素,_rObj就需要用RText包裹 +
    + 111 222 + {_rObj} +
    + ); + }; + + render(, container); + expect(ref1.current.innerHTML).toEqual('111 222blue'); + rObj.set('red'); + expect(fn).toHaveBeenCalledTimes(1); + expect(ref1.current.innerHTML).toEqual('111 222red'); + }); +}); diff --git a/packages/inula-reactive/scripts/__tests__/ReactivityTest/component/reactive-component-show.test.js b/packages/inula-reactive/scripts/__tests__/ReactivityTest/component/reactive-component-show.test.js new file mode 100644 index 00000000..ba02ae46 --- /dev/null +++ b/packages/inula-reactive/scripts/__tests__/ReactivityTest/component/reactive-component-show.test.js @@ -0,0 +1,200 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import Inula, { render, createRef, act, useReactive, useCompute, reactive, Show } from '../../../../src/index'; + +describe('测试 Show 组件', () => { + it('if为primitive值', () => { + let rObj; + const ref1 = createRef(); + const ref2 = createRef(); + const fn = jest.fn(); + const App = () => { + const _rObj = useReactive('blue'); + rObj = _rObj; + + fn(); + + return ( + // 如果else中的dom和children一个类型,需要增加key,否则会被框架当作同一个dom + + Loading... +
    + } + > +
    + {_rObj} +
    +
    + ); + }; + + render(, container); + expect(ref1.current.innerHTML).toEqual('blue'); + rObj.set(''); + expect(ref2.current.innerHTML).toEqual('Loading...'); + expect(fn).toHaveBeenCalledTimes(1); + }); + + it('if为primitive值,没有else', () => { + let rObj; + const ref1 = createRef(); + const ref2 = createRef(); + const fn = jest.fn(); + const App = () => { + const _rObj = useReactive('blue'); + rObj = _rObj; + + fn(); + + return ( + // 如果else中的dom和children一个类型,需要增加key,否则会被框架当作同一个dom + +
    {_rObj}
    +
    + ); + }; + + render(, container); + expect(ref1.current.innerHTML).toEqual('blue'); + rObj.set(''); + expect(ref2.current).toEqual(null); + expect(fn).toHaveBeenCalledTimes(1); + }); + + it('if为reactive object值', () => { + let rObj; + const ref1 = createRef(); + const ref2 = createRef(); + const fn = jest.fn(); + const App = () => { + const _rObj = useReactive({ + color: 'blue', + }); + rObj = _rObj; + + fn(); + + return ( + // 如果else中的dom和children一个类型,需要增加key,否则会被框架当作同一个dom + + Loading... +
    + } + > +
    + {_rObj.color} +
    +
    + ); + }; + + render(, container); + expect(ref1.current.innerHTML).toEqual('blue'); + rObj.color.set(''); + expect(ref2.current.innerHTML).toEqual('Loading...'); + expect(fn).toHaveBeenCalledTimes(1); + }); + + it('if为函数', () => { + let rObj; + const ref1 = createRef(); + const ref2 = createRef(); + const fn = jest.fn(); + const App = () => { + const _rObj = useReactive({ + color: 'blue', + }); + rObj = _rObj; + + fn(); + + return ( + // 如果else中的dom和children一个类型,需要增加key,否则会被框架当作同一个dom + _rObj.color} + else={ +
    + Loading... +
    + } + > +
    + {_rObj.color} +
    +
    + ); + }; + + render(, container); + expect(ref1.current.innerHTML).toEqual('blue'); + rObj.color.set(''); + expect(ref2.current.innerHTML).toEqual('Loading...'); + expect(fn).toHaveBeenCalledTimes(1); + }); + + it('if的children、else是函数', () => { + const ref1 = createRef(); + const ref2 = createRef(); + const fn = jest.fn(); + const _count = reactive(0); + const _rObj = reactive({ + color: 'blue', + }); + + const App = () => { + fn(); + + return ( + // 如果else中的dom和children一个类型,需要增加key,否则会被框架当作同一个dom + _rObj.color} + else={() => ( +
    + Loading... +
    + )} + > + {() => { + const text = useCompute(() => { + return _rObj.color.get() + _count.get(); + }); + + return ( +
    + {text} +
    + ); + }} +
    + ); + }; + + render(, container); + expect(ref1.current.innerHTML).toEqual('blue0'); + // 修改children函数中使用到的响应式变量,也会触发Show组件更新 + _count.set(1); + expect(ref1.current.innerHTML).toEqual('blue1'); + _rObj.color.set(''); + expect(ref2.current.innerHTML).toEqual('Loading...'); + expect(fn).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/inula-reactive/scripts/__tests__/ReactivityTest/component/reactive-component-switch.test.js b/packages/inula-reactive/scripts/__tests__/ReactivityTest/component/reactive-component-switch.test.js new file mode 100644 index 00000000..de736293 --- /dev/null +++ b/packages/inula-reactive/scripts/__tests__/ReactivityTest/component/reactive-component-switch.test.js @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import Inula, { render, createRef, act, useReactive, Show, Switch } from '../../../../src/index'; + +describe('测试 Switch 组件', () => { + it('Switch 配合 Show 使用', () => { + let rObj; + const refBlue = createRef(); + const refRed = createRef(); + const refYellow = createRef(); + const refNothing = createRef(); + const fn = jest.fn(); + const App = () => { + const _rObj = useReactive('blue'); + rObj = _rObj; + + fn(); + + return ( + nothing
    }> + {/*if不能写成 _rObj === 'red' 或者 _rObj.get() === 'red' */} + _rObj.get() === 'blue'}> +
    + {_rObj} +
    +
    + _rObj.get() === 'red'}> +
    + {_rObj} +
    +
    + _rObj.get() === 'yellow'}> +
    + {_rObj} +
    +
    + + ); + }; + + render(, container); + expect(refBlue.current.innerHTML).toEqual('blue'); + // rObj被3个RContext依赖,分别是Switch组件、Show组件、div[id=1]的Children + expect(rObj.usedRContexts.size).toEqual(3); + + act(() => { + rObj.set('red'); + }); + expect(refRed.current.innerHTML).toEqual('red'); + // rObj被3个Effect依赖,分别是Switch组件、Show组件、div[id=2]的Children + expect(rObj.usedRContexts.size).toEqual(3); + + act(() => { + rObj.set('black'); + }); + expect(refNothing.current.innerHTML).toEqual('nothing'); + // rObj被1个RContext依赖,分别是Switch组件 + expect(rObj.usedRContexts.size).toEqual(1); + + expect(fn).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/inula-reactive/scripts/__tests__/ReactivityTest/computed.test.js b/packages/inula-reactive/scripts/__tests__/ReactivityTest/computed.test.js new file mode 100644 index 00000000..78ddac3c --- /dev/null +++ b/packages/inula-reactive/scripts/__tests__/ReactivityTest/computed.test.js @@ -0,0 +1,51 @@ +import Inula, { computed, createRef, reactive, render } from '../../../src/index'; + +describe('测试 computed', () => { + it('在class组件render中使用computed', () => { + let rObj; + let appInst; + const ref = createRef(); + const fn = jest.fn(); + + class App extends Inula.Component { + constructor(props) { + super(props); + + appInst = this; + + this.state = { + name: 1, + }; + + this._rObj = reactive(1); + rObj = this._rObj; + } + + render() { + const computedVal = computed(() => { + fn(); + return this._rObj.get() + '!!!'; + }); + + return
    {computedVal}
    ; + } + } + + render(, container); + expect(ref.current.innerHTML).toEqual('1!!!'); // computed执行2次 + expect(fn).toHaveBeenCalledTimes(1); + rObj.set('2'); + expect(ref.current.innerHTML).toEqual('2!!!'); + expect(fn).toHaveBeenCalledTimes(2); // computed执行2次 + + // 触发组件重新渲染 + appInst.setState({ name: 2 }); + + expect(fn).toHaveBeenCalledTimes(3); // 生成新的一个computation,再执行了1次,computed总共执行3次 + + rObj.set('3'); + expect(ref.current.innerHTML).toEqual('3!!!'); + + expect(fn).toHaveBeenCalledTimes(5); // 两个computation各执行了一次,computed总共执行5次 + }); +}); diff --git a/packages/inula-reactive/scripts/__tests__/ReactivityTest/reactive-compute.test.js b/packages/inula-reactive/scripts/__tests__/ReactivityTest/reactive-compute.test.js new file mode 100644 index 00000000..716d36cb --- /dev/null +++ b/packages/inula-reactive/scripts/__tests__/ReactivityTest/reactive-compute.test.js @@ -0,0 +1,377 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import Inula, { + createRef, + For, + reactive, + render, + useCompute, + useReactive, + computed, +} from '../../../src/index'; + +describe('computed 基本使用', () => { + it('computed 返回的是一个响应式对象,用到的响应式对象是原始类型', () => { + const rObj = reactive('123'); + const comp = computed(() => { + return rObj.get() + '!!!'; + }); + expect(comp.get()).toEqual('123!!!'); + + rObj.set('456'); + expect(comp.get()).toEqual('456!!!'); + }); + + it('computed 返回的是一个响应式对象,用到两个响应式对象', () => { + const rObj1 = reactive({ name: 'xiaoming' }); + const rObj2 = reactive({ age: 18 }); + const comp = computed(() => { + return rObj1.name.get() + ' is ' + rObj2.age.get(); + }); + expect(comp.get()).toEqual('xiaoming is 18'); + rObj1.name.set('xiaowang'); + rObj2.set(prev => ({ age: prev.age + 2 })); + expect(comp.get()).toEqual('xiaowang is 20'); + }); + + it('computed 返回的是一个复杂响应式对象', () => { + const rObj = reactive({ array: [1, 2, 3, 4, 5, 6] }); + const comp = computed(() => { + return { newArray: rObj.array.get().filter(x => x > 4) }; + }); + expect(comp.get()).toEqual({ newArray: [5, 6] }); + expect(comp.newArray.get()).toEqual([5, 6]); + rObj.array.push(...[100]); + expect(comp.get()).toEqual({ newArray: [5, 6, 100] }); + }); + + it('computed 返回的是一个响应式对象,用到的响应式对象是对象类型', () => { + const rObj = reactive({ array: [1, 2, 3, 4, 5, 6] }); + const comp = computed(() => { + return rObj.array.get().filter(x => x > 4); + }); + expect(comp.get()).toEqual([5, 6]); + + rObj.array.set([1, 2, 3, 4, 5, 6, 7, 8, 9]); + expect(comp.get()).toEqual([5, 6, 7, 8, 9]); + + rObj.array.push(...[10, 11]); + expect(comp.get()).toEqual([5, 6, 7, 8, 9, 10, 11]); + + rObj.set({ array: [100, 101, 102] }); + expect(comp.get()).toEqual([100, 101, 102]); + }); + + it('computed 返回的是一个复杂响应式对象2', () => { + const rObj = reactive({ array: [1, 2, 3, 4, 5, 6] }); + const comp = computed(() => { + return { newArray: rObj.array.get().filter(x => x > 4) }; + }); + expect(comp.newArray.get()).toEqual([5, 6]); + rObj.array.push(...[7, 8]); + expect(comp.newArray.get()).toEqual([5, 6, 7, 8]); + rObj.array.set([1, 100, 101, 102]); + expect(comp.newArray.get()).toEqual([100, 101, 102]); + expect(comp.get()).toEqual({ newArray: [100, 101, 102] }); + }); +}); + +describe('测试 useCompute', () => { + it('useComputed基本使用 使用get方法(组件式更新)', () => { + let rObj; + const ref = createRef(); + const fn = jest.fn(); + const App = () => { + const _rObj = useReactive('123'); + rObj = _rObj; + + const _cObj = useCompute(() => { + return _rObj.get() + '!!!'; + }); + fn(); + + return
    {_cObj.get()}
    ; + }; + + render(, container); + expect(ref.current.innerHTML).toEqual('123!!!'); + expect(fn).toHaveBeenCalledTimes(1); + rObj.set('456'); + expect(ref.current.innerHTML).toEqual('456!!!'); + expect(fn).toHaveBeenCalledTimes(2); + rObj.set('789'); + expect(ref.current.innerHTML).toEqual('789!!!'); + expect(fn).toHaveBeenCalledTimes(3); + }); + + it('useComputed基本使用 直接使用对象(Dom级更新)', () => { + let rObj; + const ref = createRef(); + const fn = jest.fn(); + const App = () => { + const _rObj = useReactive('123'); + rObj = _rObj; + + const _cObj = useCompute(() => { + return _rObj.get() + '!!!'; + }); + fn(); + + return
    {_cObj}
    ; + }; + + render(, container); + expect(ref.current.innerHTML).toEqual('123!!!'); + expect(fn).toHaveBeenCalledTimes(1); + rObj.set('456'); + expect(ref.current.innerHTML).toEqual('456!!!'); + rObj.set('789'); + expect(ref.current.innerHTML).toEqual('789!!!'); + expect(fn).toHaveBeenCalledTimes(1); + }); + + it('useComputed 基本使用2', () => { + let rObj; + const ref = createRef(); + const fn = jest.fn(); + const compFn = jest.fn(); + const App = () => { + const _rObj = useReactive({ array: [1, 2, 3, 4, 5, 6] }); + rObj = _rObj; + + const cObj = useCompute(() => { + compFn(); + return { len: _rObj.array.get().filter(x => x >= 4).length }; + }); + + fn(); + + return
    {cObj.len}
    ; + }; + + render(, container); + expect(ref.current.innerHTML).toEqual('3'); + rObj.array.push(...[7, 8]); + expect(ref.current.innerHTML).toEqual('5'); + expect(fn).toHaveBeenCalledTimes(1); + rObj.array.unshift(...[0, 100]); + expect(ref.current.innerHTML).toEqual('6'); + rObj.set({ array: [1, 100, 101, 102, 103] }); + expect(ref.current.innerHTML).toEqual('4'); + expect(compFn).toHaveBeenCalledTimes(4); + expect(fn).toHaveBeenCalledTimes(1); + }); + + it('连锁useComputed使用', () => { + let rObj; + const ref = createRef(); + const fn = jest.fn(); + const compFn = jest.fn(); + const App = () => { + const _rObj = useReactive(1); + rObj = _rObj; + + const double = useCompute(() => _rObj.get() * 2); + const dd = useCompute(() => { + compFn(); + return double.get() * 2; + }); + + fn(); + + return
    {dd}
    ; + }; + + render(, container); + expect(ref.current.innerHTML).toEqual('4'); + expect(compFn).toHaveBeenCalledTimes(1); + rObj.set('2'); + expect(ref.current.innerHTML).toEqual('8'); + expect(compFn).toHaveBeenCalledTimes(2); + rObj.set('4'); + expect(ref.current.innerHTML).toEqual('16'); + expect(compFn).toHaveBeenCalledTimes(3); + expect(fn).toHaveBeenCalledTimes(1); + }); + + it('useComputed中使用到了两个响应式对象', () => { + let _rObj1; + let _rObj2; + const ref = createRef(); + const fn = jest.fn(); + const compFn = jest.fn(); + const App = () => { + const rObj1 = useReactive({ name: 'xiaoming' }); + const rObj2 = useReactive({ age: 18 }); + _rObj1 = rObj1; + _rObj2 = rObj2; + + const words = useCompute(() => { + compFn(); + return `${rObj1.name.get()} is ${rObj2.age.get()}`; + }); + + fn(); + + return
    {words}
    ; + }; + + render(, container); + expect(ref.current.innerHTML).toEqual('xiaoming is 18'); + expect(compFn).toHaveBeenCalledTimes(1); + _rObj1.name.set('xiaowang'); + expect(ref.current.innerHTML).toEqual('xiaowang is 18'); + expect(compFn).toHaveBeenCalledTimes(2); + _rObj2.set({ age: 20 }); + expect(ref.current.innerHTML).toEqual('xiaowang is 20'); + expect(compFn).toHaveBeenCalledTimes(3); + _rObj1.name.set('laowang'); + _rObj2.set({ age: 30 }); + expect(ref.current.innerHTML).toEqual('laowang is 30'); + expect(compFn).toHaveBeenCalledTimes(5); + expect(fn).toHaveBeenCalledTimes(1); + }); + + it('多个reactive的compute', () => { + let a; + const ref = createRef(); + const compFn = jest.fn(); + const computeFn = jest.fn(); + const App = () => { + const _a = useReactive('a'); + const b = useReactive('b'); + const cond = useReactive(true); + a = _a; + const compute = useCompute(() => { + computeFn(); + return cond.get() ? _a.get() : b.get(); + }); + + compFn(); + + return ( + + ); + }; + render(, container); + expect(ref.current.innerHTML).toEqual('a'); + ref.current.click(); + expect(ref.current.innerHTML).toEqual('b'); + a.set('aa'); + + expect(computeFn).toHaveBeenCalledTimes(3); + + expect(ref.current.innerHTML).toEqual('b'); + }); + + it('useCompute返回一个数组对象', () => { + let rObj; + let cObj; + let ref = createRef(); + let appFn = jest.fn(); + let itemFn = jest.fn(); + + const App = () => { + const _rObj = useReactive([ + { id: 'id-1', name: 'p1' }, + { id: 'id-2', name: 'p2' }, + { id: 'id-3', name: 'p3' }, + ]); + rObj = _rObj; + + const _cObj = useCompute(() => { + return _rObj.get().slice(); + }); + cObj = _cObj; + + appFn(); + + return ( +
    + + {item => { + itemFn(); + return ( +
  • + {item.name} +
  • + ); + }} +
    +
    + ); + }; + + render(, container); + let items = container.querySelectorAll('li'); + expect(items.length).toEqual(3); + + rObj.push({ id: 'id-4', name: 'p4' }); + + items = container.querySelectorAll('li'); + expect(items.length).toEqual(4); + + // rObj[1].name.get(); + rObj[1].set({ id: 'id-2', name: 'p222' }); + let li = container.querySelector('#id-2'); + + expect(li.innerHTML).toEqual('p222'); + + // // 更新 + // cObj.set([true]); + // + // items = container.querySelectorAll('li'); + // expect(items.length).toEqual(1); + // expect(appFn).toHaveBeenCalledTimes(1); + // + // // 第一次渲染执行3次,更新也触发了1次 + // expect(itemFn).toHaveBeenCalledTimes(4); + }); + + xit('测试compute在checkbox中的使用', () => { + let a; + const ref = createRef(); + const compFn = jest.fn(); + const computeFn = jest.fn(); + const App = () => { + const rObj = useReactive({ checked: true }); + const checked = useCompute(() => { + return rObj.checked.get(); + }); + + compFn(); + + return ; + }; + render(, container); + expect(ref.current.innerHTML).toEqual('a'); + ref.current.click(); + expect(ref.current.innerHTML).toEqual('b'); + a.set('aa'); + + expect(computeFn).toHaveBeenCalledTimes(3); + + expect(ref.current.innerHTML).toEqual('b'); + }); +}); diff --git a/packages/inula-reactive/scripts/__tests__/ReactivityTest/reactive-memory.test.js b/packages/inula-reactive/scripts/__tests__/ReactivityTest/reactive-memory.test.js new file mode 100644 index 00000000..70877a52 --- /dev/null +++ b/packages/inula-reactive/scripts/__tests__/ReactivityTest/reactive-memory.test.js @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import Inula, { render, createRef, act, useReactive } from '../../../src/index'; +import { Show } from '../../../src/reactive/components/Show'; +import { Switch } from '../../../src/reactive/components/Switch'; + +describe('响应式数据usedRContexts', () => { + it('测试响应式数据的usedRContexts会随着VNode的删除而清除', () => { + let rObj; + const refBlue = createRef(); + const refRed = createRef(); + const refYellow = createRef(); + const refNothing = createRef(); + const fn = jest.fn(); + const App = () => { + const _rObj = useReactive('blue'); + rObj = _rObj; + + fn(); + + return ( + nothing
    }> + {/*if不能写成 _rObj === 'red' 或者 _rObj.get() === 'red' */} + _rObj.get() === 'blue'}> +
    + {_rObj} +
    +
    + _rObj.get() === 'red'}> +
    + {_rObj} +
    +
    + _rObj.get() === 'yellow'}> +
    + {_rObj} +
    +
    + + ); + }; + + render(, container); + expect(refBlue.current.innerHTML).toEqual('blue'); + // rObj被3个RContext依赖,分别是Switch组件、Show组件、div[id=1]的Children + expect(rObj.usedRContexts.size).toEqual(3); + + act(() => { + rObj.set('red'); + }); + expect(refRed.current.innerHTML).toEqual('red'); + // rObj被3个Effect依赖,分别是Switch组件、Show组件、div[id=2]的Children + expect(rObj.usedRContexts.size).toEqual(3); + + act(() => { + rObj.set('black'); + }); + expect(refNothing.current.innerHTML).toEqual('nothing'); + // rObj被1个RContext依赖,分别是Switch组件 + expect(rObj.usedRContexts.size).toEqual(1); + + expect(fn).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/inula-reactive/scripts/__tests__/ReactivityTest/reactive-mix-use.test.js b/packages/inula-reactive/scripts/__tests__/ReactivityTest/reactive-mix-use.test.js new file mode 100644 index 00000000..ef145de7 --- /dev/null +++ b/packages/inula-reactive/scripts/__tests__/ReactivityTest/reactive-mix-use.test.js @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import Inula, {createRef, render, useReactive, useState, Show} from '../../../src/index'; + +describe('传统API和响应式API混合使用', () => { + it('混合使用1', () => { + let rObj, isShow, update; + const ref = createRef(); + const fn = jest.fn(); + + const App = () => { + const _isShow = useReactive(true); + isShow = _isShow; + + const [_, setState] = useState({}); + + update = () => setState({}); + return ( + + + + ); + }; + + const Child = () => { + const _rObj = useReactive('blue'); + rObj = _rObj; + + fn(); + + return
    ; + }; + + render(, container); + expect(ref.current.className).toEqual('blue'); + + // 改变了DOM结构 + isShow.set(false); + expect(ref.current).toEqual(null); + + update(); + + expect(ref.current).toEqual(null); + }); + + +}); diff --git a/packages/inula-reactive/scripts/__tests__/ReactivityTest/reactive-mixed-children.test.js b/packages/inula-reactive/scripts/__tests__/ReactivityTest/reactive-mixed-children.test.js new file mode 100644 index 00000000..c6d81e22 --- /dev/null +++ b/packages/inula-reactive/scripts/__tests__/ReactivityTest/reactive-mixed-children.test.js @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import Inula, { render, createRef, act, useReactive } from '../../../src/index'; + +describe('测试混合型的 children', () => { + it('children是 字符串+Atom 场景', () => { + let rObj; + const ref1 = createRef(); + const fn = jest.fn(); + const App = () => { + const _rObj = useReactive(0); + rObj = _rObj; + + fn(); + + return ( + // div下面有多个元素 +
    Count: {_rObj}
    + ); + }; + + render(, container); + expect(ref1.current.innerHTML).toEqual('Count: 0'); + rObj.set(1); + expect(fn).toHaveBeenCalledTimes(1); + expect(ref1.current.innerHTML).toEqual('Count: 1'); + }); + + it('children是 字符串+Atom 场景2', () => { + let rObj; + const ref1 = createRef(); + const fn = jest.fn(); + const App = () => { + const _rObj = useReactive({ count: 0 }); + rObj = _rObj; + + fn(); + + return ( + // div下面有多个元素 +
    Count: {_rObj.count}
    + ); + }; + + render(, container); + expect(ref1.current.innerHTML).toEqual('Count: 0'); + rObj.count.set(1); + expect(fn).toHaveBeenCalledTimes(1); + expect(ref1.current.innerHTML).toEqual('Count: 1'); + }); +}); diff --git a/packages/inula-reactive/scripts/__tests__/ReactivityTest/reactive-object.test.js b/packages/inula-reactive/scripts/__tests__/ReactivityTest/reactive-object.test.js new file mode 100644 index 00000000..90ae29fc --- /dev/null +++ b/packages/inula-reactive/scripts/__tests__/ReactivityTest/reactive-object.test.js @@ -0,0 +1,973 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import Inula, { + render, + createRef, + useReactive, + useCompute, + reactive, + computed, + watchReactive, +} from '../../../src/index'; +import { GET_R_NODE } from '../../../src/reactive/proxy/RProxyHandler'; +import { isAtom, isReactiveProxy, isRNode } from '../../../src/reactive/Utils'; + +describe('测试 useReactive(对象)', () => { + it('reactive基本使用', () => { + let rObj; + const ref = createRef(); + const fn = jest.fn(); + const App = () => { + const _rObj = useReactive({ + color: 'blue', + }); + rObj = _rObj; + + fn(); + + return
    {_rObj.color}
    ; + }; + + render(, container); + expect(ref.current.innerHTML).toEqual('blue'); + rObj.color.set('red'); + expect(rObj.color.get()).toEqual('red'); + expect(ref.current.innerHTML).toEqual('red'); + rObj.color.set(prev => prev + '!!'); + expect(rObj.color.get()).toEqual('red!!'); + expect(ref.current.innerHTML).toEqual('red!!'); + expect(fn).toHaveBeenCalledTimes(1); + }); + + it('响应式对象赋值修改为一个对象', () => { + let rObj; + const ref = createRef(); + const fn = jest.fn(); + const App = () => { + const _rObj = useReactive({ + data: { framework: 'Vue' }, + }); + rObj = _rObj; + + fn(); + + return
    {_rObj.data.framework}
    ; + }; + + render(, container); + expect(ref.current.innerHTML).toEqual('Vue'); + rObj.data.set({ framework: 'React' }); + expect(rObj.data.framework.get()).toEqual('React'); + expect(ref.current.innerHTML).toEqual('React'); + expect(fn).toHaveBeenCalledTimes(1); + }); + + it('赋值修改复杂响应式对象', () => { + let rObj; + const ref = createRef(); + const fn = jest.fn(); + const App = () => { + const _rObj = useReactive({ + data: { framework: { js: 'Vue' } }, + }); + rObj = _rObj; + + fn(); + + return
    {_rObj.data.framework.js}
    ; + }; + + render(, container); + expect(ref.current.innerHTML).toEqual('Vue'); + rObj.data.set({ framework: { js: 'React' } }); + expect(rObj.data.framework.get()).toEqual({ js: 'React' }); + expect(ref.current.innerHTML).toEqual('React'); + expect(fn).toHaveBeenCalledTimes(1); + }); + + it('赋值修改响应式对象中Atom的值', () => { + let rObj; + const ref = createRef(); + const fn = jest.fn(); + const App = () => { + const _rObj = useReactive({ + rdata: { framework: 'Vue' }, + }); + rObj = _rObj; + + fn(); + + return
    {_rObj.rdata.framework}
    ; + }; + + render(, container); + expect(ref.current.innerHTML).toEqual('Vue'); + rObj.rdata.framework.set('React'); + expect(rObj.rdata.get()).toEqual({ framework: 'React' }); + expect(ref.current.innerHTML).toEqual('React'); + expect(fn).toHaveBeenCalledTimes(1); + }); + + it('把响应式属性传递到子组件', () => { + let rObj; + const ref = createRef(); + const fn1 = jest.fn(); + const fn2 = jest.fn(); + + const App = () => { + const _rObj = useReactive({ + data: { + color: 'blue', + }, + }); + rObj = _rObj; + + fn1(); + return ; + }; + + const Child = ({ color }) => { + fn2(); + + const cl = useCompute(() => { + return 'cl-' + color.get(); + }); + + return
    ; + }; + + render(, container); + expect(ref.current.className).toEqual('cl-blue'); + expect(fn1).toHaveBeenCalledTimes(1); + expect(fn2).toHaveBeenCalledTimes(1); + rObj.data.color.set('red'); + expect(fn1).toHaveBeenCalledTimes(1); + expect(fn2).toHaveBeenCalledTimes(1); + expect(ref.current.className).toEqual('cl-red'); + }); + + it('reactive对象中“原始数据”被赋值为“对象”', () => { + let rObj; + const ref = createRef(); + const fn = jest.fn(); + const App = () => { + const _rObj = useReactive({ + data: 'blue', + }); + rObj = _rObj; + + _rObj.data.set({ color: 'red' }); + + fn(); + + return
    {_rObj.data.color}
    ; + }; + + render(, container); + expect(ref.current.innerHTML).toEqual('red'); + rObj.data.color.set('blue'); + expect(rObj.data.color.get()).toEqual('blue'); + expect(ref.current.innerHTML).toEqual('blue'); + expect(fn).toHaveBeenCalledTimes(1); + }); + + it('reactive对象中“对象”被赋值为“新对象”', () => { + let rObj; + const ref = createRef(); + const fn = jest.fn(); + const App = () => { + const _rObj = useReactive({ + data: { + cl: 'blue', + }, + }); + rObj = _rObj; + + _rObj.data.set({ color: 'red' }); + + fn(); + + return
    {_rObj.data.color}
    ; + }; + + render(, container); + expect(ref.current.innerHTML).toEqual('red'); + rObj.data.color.set('blue'); + expect(rObj.data.color.get()).toEqual('blue'); + expect(ref.current.innerHTML).toEqual('blue'); + expect(fn).toHaveBeenCalledTimes(1); + }); +}); + +describe('测试reactive数组', () => { + it('reactive“数组”length的使用', () => { + let rObj; + const ref = createRef(); + const fn = jest.fn(); + const App = () => { + const _rObj = useReactive({ + data: [ + { name: 'p1', age: 1 }, + { name: 'p2', age: 2 }, + ], + }); + rObj = _rObj; + + fn(); + + // 在DOM中使用length无法精细响应式 + return
    {_rObj.data.length}
    ; + }; + + render(, container); + expect(ref.current.innerHTML).toEqual('2'); + rObj.data.set([{ name: 'p1', age: 1 }]); + expect(ref.current.innerHTML).toEqual('1'); + expect(fn).toHaveBeenCalledTimes(2); + }); + + it('reactive“数组”的使用', () => { + let rObj; + const ref = createRef(); + const fn = jest.fn(); + const App = () => { + const _rObj = useReactive({ + data: [ + { name: 'p1', age: 1 }, + { name: 'p2', age: 2 }, + ], + }); + rObj = _rObj; + + fn(); + + return
    {_rObj.data[0].name}
    ; + }; + + render(, container); + expect(ref.current.innerHTML).toEqual('p1'); + // 这种修改无法响应! + // rObj.data.set([ + // { name: 'p11', age: 1 }, + // ]); + + // 直接修改数组中被使用属性 + rObj.data[0].name.set('p11'); + expect(ref.current.innerHTML).toEqual('p11'); + // 在DOM中使用length无法精细响应式 + expect(fn).toHaveBeenCalledTimes(1); + }); + + it('jsx中通过items.get().map遍历reactive“数组”', () => { + let rObj; + const ref = createRef(); + const fn = jest.fn(); + const App = () => { + const _rObj = useReactive({ + items: [ + { name: 'p1', id: 1 }, + { name: 'p2', id: 2 }, + ], + }); + rObj = _rObj; + + fn(); + + return ( +
    + {_rObj.items.get().map(item => { + return
  • {item.name}
  • ; + })} +
    + ); + }; + + render(, container); + let items = container.querySelectorAll('li'); + expect(items.length).toEqual(2); + + // 每次修改items都会触发整个组件刷新 + rObj.items.set([{ name: 'p11', age: 1 }]); + + items = container.querySelectorAll('li'); + expect(items.length).toEqual(1); + expect(fn).toHaveBeenCalledTimes(2); + + // 每次修改items都会触发整个组件刷新 + rObj.items.push({ name: 'p22', id: 2 }); + + items = container.querySelectorAll('li'); + expect(items.length).toEqual(2); + expect(fn).toHaveBeenCalledTimes(3); + }); + + it('jsx中通过items.get().map遍历reactive“数组”,孩子是Item', () => { + let rObj; + const ref = createRef(); + const fn = jest.fn(); + const Item = ({ item }) => { + return
  • {item.name}
  • ; + }; + + const App = () => { + const _rObj = useReactive({ + items: [ + { name: 'p1', id: 1 }, + { name: 'p2', id: 2 }, + ], + }); + rObj = _rObj; + + fn(); + + return ( +
    + {/*items必须要调用get()才能map*/} + {_rObj.items.get().map(item => { + return ; + })} +
    + ); + }; + + render(, container); + let items = container.querySelectorAll('li'); + expect(items.length).toEqual(2); + expect(fn).toHaveBeenCalledTimes(1); + + // 每次修改items都会触发整个组件刷新 + rObj.items.set([{ name: 'p11', age: 1 }]); + + items = container.querySelectorAll('li'); + expect(items.length).toEqual(1); + expect(fn).toHaveBeenCalledTimes(2); + + // 每次修改items都会触发整个组件刷新 + rObj.items.push({ name: 'p22', id: 2 }); + + items = container.querySelectorAll('li'); + expect(items.length).toEqual(2); + expect(fn).toHaveBeenCalledTimes(3); + }); + + it('jsx中通过items.map遍历reactive“数组”,具有响应式', () => { + let rObj; + const ref = createRef(); + const fn = jest.fn(); + const Item = ({ item }) => { + return
  • {item.name}
  • ; + }; + + const App = () => { + const _rObj = useReactive({ + items: [ + { name: 'p1', id: 1 }, + { name: 'p2', id: 2 }, + { name: 'p3', id: 3 }, + ], + }); + rObj = _rObj; + + fn(); + + return ( +
    + {_rObj.items.map(item => { + return ; + })} +
    + ); + }; + + render(, container); + let items = container.querySelectorAll('li'); + expect(items.length).toEqual(3); + expect(fn).toHaveBeenCalledTimes(1); + + rObj.items.set([ + { name: 'p11', age: 1 }, + { name: 'p22', age: 2 }, + ]); + + items = container.querySelectorAll('li'); + // 子元素不会响应式变化 + expect(items.length).toEqual(2); + expect(fn).toHaveBeenCalledTimes(2); + }); + + it('jsx中通过items.map遍历reactive“数组”,孩子是Item,Item对象具有响应式', () => { + let rObj; + const ref = createRef(); + const fn = jest.fn(); + const Item = ({ item }) => { + const id = useCompute(() => { + return `id-${item.id.get()}`; + }); + + return ( +
  • + {item.name} +
  • + ); + }; + + const App = () => { + const _rObj = useReactive({ + items: [ + { name: 'p1', id: 1 }, + { name: 'p2', id: 2 }, + ], + }); + rObj = _rObj; + + fn(); + + return ( +
    + {_rObj.items.map(item => { + return ; + })} +
    + ); + }; + + render(, container); + let item = container.querySelector('#id-1'); + expect(item.innerHTML).toEqual('p1'); + expect(fn).toHaveBeenCalledTimes(1); + + rObj.items[0].name.set('p111'); + item = container.querySelector('#id-1'); + // 子元素会响应式变化 + expect(item.innerHTML).toEqual('p111'); + expect(fn).toHaveBeenCalledTimes(1); + }); + + it('测试响应式数据', () => { + const obj = reactive({ + data: [ + { + id: '1', + value: 'val-1', + }, + { + id: '2', + value: 'val-2', + }, + ], + }); + + // 使用,让创建children + obj.data[1].value.read(); + + obj.set({ + data: [ + { + id: '11', + value: 'val-11', + }, + ], + }); + + obj.set({ + data: [ + { + id: '111', + value: 'val-111', + }, + { + id: '222', + value: 'val-222', + }, + ], + }); + + expect(obj.data[1].value.get()).toEqual('val-222'); + }); + + it('响应式对象为复杂对象时,使用set重新设置', () => { + const rObj = reactive({ data: [1, 2, 3, 4, 5, 6] }); + rObj.data.push(...[7, 8]); + expect(rObj.data.get()).toEqual([1, 2, 3, 4, 5, 6, 7, 8]); + rObj.data.set([100, 101]); + expect(rObj.get()).toEqual({ data: [100, 101] }); + }); + + it('使用set直接修改响应式对象数组中某个元素的值', () => { + const rObj = reactive({ data: [1, 2, 3] }); + rObj.data.push(...[4, 5, 6]); + expect(rObj.data.get()).toEqual([1, 2, 3, 4, 5, 6]); + + // 修改数组第4个元素 + rObj.data[1].set({ val: 2 }); + expect(rObj.get()).toEqual({ data: [1, { val: 2 }, 3, 4, 5, 6] }); + }); + + it('使用set直接修改响应式对象数组中某个元素的值2', () => { + const rObj = reactive({ data: [1, 2, 3] }); + rObj.data.push(...[4, 5, 6]); + expect(rObj.data.get()).toEqual([1, 2, 3, 4, 5, 6]); + + // 修改数组第4个元素 + rObj.data[4].set({ val: 2 }); + expect(rObj.get()).toEqual({ data: [1, 2, 3, 4, { val: 2 }, 6] }); + }); + + it('在删除数组中一个数字,再加一个对象,类型是RNode', () => { + const rObj = reactive({ data: [1, 2, 3, 4] }); + // 使用最后一个数据,在children中创建出child + rObj.data[3].get(); + // 删除最后一个数据 + rObj.data.set([1, 2, 3]); + // 重新增加一个obj类型的数据 + rObj.data.set([1, 2, 3, { val: 4 }]); + + // rObj.data[3]是RNode + expect(isRNode(rObj.data[3][GET_R_NODE])).toBeTruthy(); + + expect(rObj.data[3].val.get()).toEqual(4); + }); + + xit('钻石问题', () => { + const fn = jest.fn(); + const rObj = reactive(0); + const evenOrOdd = computed(() => (rObj.get() % 2 === 0 ? 'even' : 'odd')); + + watchReactive(() => { + fn(); + rObj.get(); + evenOrOdd.get(); + }); + + rObj.set(1); + + // TODO + expect(fn).toHaveBeenCalledTimes(3); + }); + + it('数组中的数据由“对象”变成“字符串”', () => { + let fn = jest.fn(); + let fn1 = jest.fn(); + const rObj = reactive({ + items: [ + { name: 'p1', id: 1 }, + { name: { n: 'p22' }, id: 2 }, + ], + }); + + watchReactive(rObj.items[1].name, () => { + fn(); + }); + watchReactive(rObj.items[1].name.n, () => { + fn1(); + }); + + rObj.items.set([ + { name: 'p1', id: 1 }, + { name: 'p2', id: 2 }, // name 改为 基本数据类型 + ]); + + expect(fn).toHaveBeenCalledTimes(1); + + // 无法触发fn1 + expect(fn1).toHaveBeenCalledTimes(0); + }); + + it('数组中的数据由“字符串”变成“对象”', () => { + let fn = jest.fn(); + let fn1 = jest.fn(); + const rObj = reactive({ + items: [ + { name: 'p1', id: 1 }, + { name: 'p2', id: 2 }, + ], + }); + + watchReactive(rObj.items[1].name, () => { + fn(); + }); + // 允许使用或监听没有定义的属性 + watchReactive(rObj.items[1].name.n, () => { + fn1(); + }); + + rObj.items.set([ + { name: 'p1', id: 1 }, + { name: { n: 'p22' }, id: 2 }, + ]); + + expect(fn).toHaveBeenCalledTimes(1); + + // 可以触发fn1 + expect(fn1).toHaveBeenCalledTimes(1); + }); + + it('访问一个不存在的属性,会抛出异常', () => { + let fn = jest.fn(); + let fn1 = jest.fn(); + const rObj = reactive({ + items: [{ name: 'p1' }, { name: 'p2' }], + }); + + watchReactive(() => { + rObj.items[1].get(); + fn(); + }); + watchReactive(() => { + // 会抛异常 + rObj.items[1].name.n.get(); + fn1(); + }); + + rObj.items.set([{ name: 'p1' }, { name: { n: 'p22' } }]); + + expect(fn).toHaveBeenCalledTimes(2); + + // 无法触发fn1 + expect(fn1).toHaveBeenCalledTimes(2); + }); + + it('数组中的数据由“字符串”变成“对象”3', () => { + let fn = jest.fn(); + let fn1 = jest.fn(); + const rObj = reactive({ + items: [{ a: 1 }, 2, 3], + }); + + watchReactive(() => { + rObj.items[1].get(); + fn(); + }); + + rObj.items.set([2, 3, 4]); + + expect(fn).toHaveBeenCalledTimes(2); + }); + + it('数组中的数据由“数字”变成“对象”', () => { + let fn = jest.fn(); + let fn1 = jest.fn(); + const rObj = reactive({ + items: [1, 2, 3], + }); + + watchReactive(() => { + rObj.items[0].get(); + fn(); + }); + + watchReactive(() => { + rObj.get(); + fn1(); + }); + + rObj.items.set([{ a: 1 }, 3, 4]); + + expect(fn).toHaveBeenCalledTimes(2); + + // 父数据也会触发 + expect(fn1).toHaveBeenCalledTimes(2); + }); + + it('数组中的数据由“对象”变成“数组”', () => { + let fn = jest.fn(); + let fn1 = jest.fn(); + const rObj = reactive({ + items: [{ a: 1 }, 2, 3], + }); + + watchReactive(() => { + rObj.items[0].get(); + fn(); + }); + + watchReactive(() => { + rObj.get(); + fn1(); + }); + + rObj.items.set([[1], 3, 4]); + + expect(fn).toHaveBeenCalledTimes(2); + + // 父数据也会触发 + expect(fn1).toHaveBeenCalledTimes(2); + }); + + it('数组中的数据由“空数组”变成“空数组”', () => { + let fn = jest.fn(); + let fn1 = jest.fn(); + const rObj = reactive({ + items: [[], 2, 3], + }); + + watchReactive(() => { + rObj.items[0].get(); + fn(); + }); + + watchReactive(() => { + rObj.get(); + fn1(); + }); + + rObj.items.set([[], 3, 4]); + + expect(fn).toHaveBeenCalledTimes(2); + + // 父数据也会触发 + expect(fn1).toHaveBeenCalledTimes(2); + }); + + it('数组中的2个数据由“对象”变成“对象”', () => { + let fn = jest.fn(); + let fn1 = jest.fn(); + const rObj = reactive({ + items: [{ a: { b: 1 }, b: { c: 2 } }, { a: 2 }, 3], + }); + + watchReactive(() => { + rObj.items[0].a.get(); + rObj.items[0].b.get(); + fn(); + }); + + watchReactive(() => { + rObj.get(); + fn1(); + }); + + // 第一个a 由{b: 1} -> {b: 2}能够精准更新 + rObj.items.set([{ a: { b: 2 }, b: { c: 3 } }, { a: 3 }, 4]); + + expect(fn).toHaveBeenCalledTimes(2); + + // 父数据也会触发 + expect(fn1).toHaveBeenCalledTimes(2); + }); + + it('数组中的2个数据由“对象”变成“对象”,前一个属性能精准更新,会触发后面那个', () => { + let fn = jest.fn(); + let fn1 = jest.fn(); + let fn2 = jest.fn(); + let fn3 = jest.fn(); + let fn4 = jest.fn(); + const rObj = reactive({ + items: [{ a: { b: 1 }, b: { c: 2 } }, { a: 2 }, 3], + }); + + watchReactive(() => { + rObj.items[0].a.get(); + fn(); + }); + watchReactive(() => { + // b由 { c: 2 } -> { c: 3 } 可以触发 + rObj.items[0].b.get(); + fn1(); + }); + watchReactive(() => { + // b由 1 -> undefined 可以触发 + rObj.items[0].a.b.get(); + fn2(); + }); + watchReactive(() => { + // c由 2 -> 3 可以触发 + rObj.items[0].b.c.get(); + fn3(); + }); + watchReactive(() => { + rObj.get(); + fn4(); + }); + + // 第一个a 由{b: 1} -> {d: 2}能够精准更新 + rObj.items.set([{ a: { d: 2 }, b: { c: 3 } }, { a: 3 }, 4]); + + expect(fn).toHaveBeenCalledTimes(2); + expect(fn1).toHaveBeenCalledTimes(2); + expect(fn2).toHaveBeenCalledTimes(2); + expect(fn3).toHaveBeenCalledTimes(2); + + // 父数据也会触发 + expect(fn4).toHaveBeenCalledTimes(2); + }); + + it('数组中的2个数据由“对象”变成“对象”,前一个属性不能精准更新,也不再触发后面那个', () => { + let fn = jest.fn(); + let fn1 = jest.fn(); + let fn2 = jest.fn(); + let fn3 = jest.fn(); + let fn4 = jest.fn(); + const rObj = reactive({ + items: [{ a: { b: 1 }, b: { c: 2 } }, { a: 2 }, 3], + }); + + watchReactive(() => { + rObj.items[0].a.get(); + fn(); + }); + watchReactive(() => { + // b由 { c: 2 } -> { c: 3 } 可以触发 + rObj.items[0].b.get(); + fn1(); + }); + watchReactive(() => { + rObj.items[0].a.b.get(); + fn2(); + }); + watchReactive(() => { + rObj.items[0].b.c.get(); + fn3(); + }); + watchReactive(() => { + rObj.get(); + fn4(); + }); + + // 第一个 a 由{b: 1} -> 1 不能够精准更新 + rObj.items.set([{ a: 1, b: { c: 3 } }, { a: 3 }, 4]); + + expect(fn).toHaveBeenCalledTimes(2); + expect(fn1).toHaveBeenCalledTimes(2); + // 由 { b: 1 } -> 1 是不会触发 b 精准更新 + expect(fn2).toHaveBeenCalledTimes(1); + // 前一个属性不能精准更新,也不触发后面那个的精准更新 + expect(fn3).toHaveBeenCalledTimes(1); + + // 父数据也会触发 + expect(fn4).toHaveBeenCalledTimes(2); + }); + + it('数组中的2个数据由“对象”变成“[]”,前一个属性不能精准更新,也不再触发后面那个', () => { + let fn = jest.fn(); + let fn1 = jest.fn(); + let fn2 = jest.fn(); + let fn3 = jest.fn(); + let fn4 = jest.fn(); + const rObj = reactive({ + items: [{ a: { b: 1 }, b: { c: 2 } }, { a: 2 }, 3], + }); + + watchReactive(() => { + rObj.items[0].a.get(); + fn(); + }); + watchReactive(() => { + // b由 { c: 2 } -> { c: 3 } 可以触发 + rObj.items[0].b.get(); + fn1(); + }); + watchReactive(() => { + rObj.items[0].a.b.get(); + fn2(); + }); + watchReactive(() => { + rObj.items[0].b.c.get(); + fn3(); + }); + watchReactive(() => { + rObj.get(); + fn4(); + }); + + // 第一个 a 由{b: 1} -> [] 不能够精准更新 + rObj.items.set([{ a: [], b: { c: 3 } }, { a: 3 }, 4]); + + expect(fn).toHaveBeenCalledTimes(2); + expect(fn1).toHaveBeenCalledTimes(2); + // 由 { b: 1 } -> 1 是不会触发 b 精准更新 + expect(fn2).toHaveBeenCalledTimes(1); + // 前一个属性不能精准更新,也不触发后面那个的精准更新 + expect(fn3).toHaveBeenCalledTimes(1); + + // 父数据也会触发 + expect(fn4).toHaveBeenCalledTimes(2); + }); + + it('数组中的2个数据由“对象”变成“null”,前一个属性不能精准更新,也不再触发后面那个', () => { + let fn = jest.fn(); + let fn1 = jest.fn(); + let fn2 = jest.fn(); + let fn3 = jest.fn(); + let fn4 = jest.fn(); + const rObj = reactive({ + items: [{ a: { b: 1 }, b: { c: 2 } }, { a: 2 }, 3], + }); + + watchReactive(() => { + rObj.items[0].a.get(); + fn(); + }); + watchReactive(() => { + // b由 { c: 2 } -> { c: 3 } 可以触发 + rObj.items[0].b.get(); + fn1(); + }); + watchReactive(() => { + rObj.items[0].a.b.get(); + fn2(); + }); + watchReactive(() => { + rObj.items[0].b.c.get(); + fn3(); + }); + watchReactive(() => { + rObj.get(); + fn4(); + }); + + // 第一个 a 由{b: 1} -> [] 不能够精准更新 + rObj.items.set([{ a: null, b: { c: 3 } }, { a: 3 }, 4]); + + expect(fn).toHaveBeenCalledTimes(2); + expect(fn1).toHaveBeenCalledTimes(2); + // 由 { b: 1 } -> 1 是不会触发 b 精准更新 + expect(fn2).toHaveBeenCalledTimes(1); + // 前一个属性不能精准更新,也不触发后面那个的精准更新 + expect(fn3).toHaveBeenCalledTimes(1); + + // 父数据也会触发 + expect(fn4).toHaveBeenCalledTimes(2); + }); + + it('执行基本数据数组的loop方法', () => { + let fn = jest.fn(); + let fn1 = jest.fn(); + const rObj = reactive({ + items: [1, 2, 3, 4], + }); + + rObj.items.forEach(rItem => { + expect(isReactiveProxy(rItem)).toBeTruthy(); + }); + + watchReactive(() => { + rObj.items.get(); + fn(); + }); + watchReactive(() => { + rObj.get(); + fn1(); + }); + + rObj.items.set([1, 2, 3]); + + expect(fn).toHaveBeenCalledTimes(2); + + // 父数据也会触发 + expect(fn1).toHaveBeenCalledTimes(2); + }); +}); diff --git a/packages/inula-reactive/scripts/__tests__/ReactivityTest/reactive-primitive.test.js b/packages/inula-reactive/scripts/__tests__/ReactivityTest/reactive-primitive.test.js new file mode 100644 index 00000000..79f44515 --- /dev/null +++ b/packages/inula-reactive/scripts/__tests__/ReactivityTest/reactive-primitive.test.js @@ -0,0 +1,266 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import Inula, { render, createRef, useState, useReactive, useCompute } from '../../../src/index'; + +describe('测试 useReactive(原生数据)', () => { + it('reactive.get()作为children', () => { + let rObj; + const ref = createRef(); + const fn = jest.fn(); + const App = () => { + const _rObj = useReactive('1'); + rObj = _rObj; + + fn(); + + return
    {_rObj.get()}
    ; + }; + render(, container); + expect(ref.current.innerHTML).toEqual('1'); + rObj.set('2'); + expect(ref.current.innerHTML).toEqual('2'); + expect(fn).toHaveBeenCalledTimes(2); + }); + + it('reactive作为children', () => { + let rObj; + const ref = createRef(); + const fn = jest.fn(); + const App = () => { + const _rObj = useReactive('1'); + rObj = _rObj; + + fn(); + + return
    {_rObj}
    ; + }; + render(, container); + expect(ref.current.innerHTML).toEqual('1'); + rObj.set('2'); + expect(ref.current.innerHTML).toEqual('2'); + rObj.set(prev => prev + '??'); + expect(ref.current.innerHTML).toEqual('2??'); + expect(fn).toHaveBeenCalledTimes(1); + }); + + it('reactive.get()作为prop', () => { + let rObj; + const ref = createRef(); + const fn = jest.fn(); + const App = () => { + const _rObj = useReactive(1); + rObj = _rObj; + + fn(); + + return
    ; + }; + render(, container); + expect(ref.current.className).toEqual('1'); + rObj.set(2); + expect(ref.current.className).toEqual('2'); + expect(fn).toHaveBeenCalledTimes(2); + }); + + it('reactive作为prop', () => { + let rObj; + const ref = createRef(); + const fn = jest.fn(); + const App = () => { + const _rObj = useReactive(1); + rObj = _rObj; + + fn(); + + return
    ; + }; + render(, container); + expect(ref.current.className).toEqual('1'); + rObj.set(2); + expect(ref.current.className).toEqual('2'); + expect(fn).toHaveBeenCalledTimes(1); + }); + + it('reactive.get()传入style', () => { + let rObj; + const ref = createRef(); + const fn = jest.fn(); + + const App = () => { + const _rObj = useReactive('blue'); + rObj = _rObj; + + fn(); + + return
    ; + }; + render(, container); + const style = window.getComputedStyle(ref.current); + expect(style.color).toEqual('blue'); + + rObj.set('red'); + expect(ref.current.style.color).toEqual('red'); + + expect(fn).toHaveBeenCalledTimes(2); + }); + + it('reactive传入style', () => { + let rObj; + const ref = createRef(); + const fn = jest.fn(); + + const App = () => { + const _rObj = useReactive('blue'); + rObj = _rObj; + + fn(); + + return
    ; + }; + render(, container); + const style = window.getComputedStyle(ref.current); + expect(style.color).toEqual('blue'); + + rObj.set('red'); + expect(ref.current.style.color).toEqual('red'); + + expect(fn).toHaveBeenCalledTimes(1); + }); + + it('reactive传入Input value', () => { + let rObj; + const ref = createRef(); + const fn = jest.fn(); + + const App = () => { + const _rObj = useReactive('blue'); + rObj = _rObj; + + fn(); + + return ; + }; + render(, container); + expect(ref.current.value).toEqual('blue'); + + rObj.set('red'); + expect(ref.current.value).toEqual('red'); + + expect(fn).toHaveBeenCalledTimes(1); + }); + + it('reactive传入Textarea value', () => { + let rObj; + const ref = createRef(); + const fn = jest.fn(); + + const App = () => { + const _rObj = useReactive('blue'); + rObj = _rObj; + + fn(); + + return ; + }; + render(, container); + expect(ref.current.value).toEqual('blue'); + + rObj.set('red'); + expect(ref.current.value).toEqual('red'); + + expect(fn).toHaveBeenCalledTimes(1); + }); + + it('reactive父组件刷新, effect不应该重新监听', () => { + let rObj, update; + const ref = createRef(); + const fn = jest.fn(); + + const App = () => { + const [_, setState] = useState({}); + + update = () => setState({}); + return ; + }; + + const Child = () => { + const _rObj = useReactive('blue'); + rObj = _rObj; + + fn(); + + return
    ; + }; + + render(, container); + expect(ref.current.className).toEqual('blue'); + expect(fn).toHaveBeenCalledTimes(1); + update(); + expect(fn).toHaveBeenCalledTimes(2); + rObj.set('red'); + expect(fn).toHaveBeenCalledTimes(2); + expect(ref.current.className).toEqual('red'); + }); + + it('不允许:从“原生数据”变成“对象”', () => { + let rObj; + const ref = createRef(); + const fn = jest.fn(); + const App = () => { + const _rObj = useReactive('1'); + rObj = _rObj; + + fn(); + + const cp = useCompute(() => { + return _rObj.get() === '1' ? '1' : _rObj.data.get(); + }); + + return
    {cp}
    ; + }; + render(, container); + expect(ref.current.innerHTML).toEqual('1'); + + // 不允许:从“原生数据”变成“对象” + expect(() => rObj.set({ data: '2' })).toThrow(Error('Not allowed Change Primitive to Object')); + }); + + it('允许:一个reactive属性从“原生数据”变成“对象”', () => { + let rObj; + const ref = createRef(); + const fn = jest.fn(); + const App = () => { + const _rObj = useReactive({ + data: '1', + }); + rObj = _rObj; + + fn(); + + const cp = useCompute(() => { + return _rObj.data.get() === '1' ? '1' : _rObj.data.num.get(); + }); + + return
    {cp}
    ; + }; + render(, container); + expect(ref.current.innerHTML).toEqual('1'); + + rObj.data.set({ num: '2' }); + expect(ref.current.innerHTML).toEqual('2'); + expect(fn).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/inula-reactive/scripts/__tests__/ReactivityTest/reactive-props-update.test.js b/packages/inula-reactive/scripts/__tests__/ReactivityTest/reactive-props-update.test.js new file mode 100644 index 00000000..e489877d --- /dev/null +++ b/packages/inula-reactive/scripts/__tests__/ReactivityTest/reactive-props-update.test.js @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import Inula, { createRef, render, useReactive } from '../../../src/index'; + +describe('测试在DOM的props中使用响应式数据', () => { + it('在class props中使用响应式数据', () => { + let rObj; + const ref = createRef(); + + const fn = jest.fn(); + const App = () => { + const _rObj = useReactive({ class: 'c1', color: 'blue' }); + rObj = _rObj; + + fn(); + + return ( +
    + {_rObj.color} +
    + ); + }; + + render(, container); + expect(ref.current.innerHTML).toEqual('blue'); + expect(ref.current.getAttribute('class')).toEqual('c1'); + + rObj.class.set('c2'); + expect(ref.current.getAttribute('class')).toEqual('c2'); + + expect(fn).toHaveBeenCalledTimes(1); + }); + + it('在style中使用响应式数据', () => { + let rObj; + const ref = createRef(); + + const fn = jest.fn(); + const App = () => { + const _rObj = useReactive({ class: 'c1', color: 'blue' }); + rObj = _rObj; + + fn(); + + return ( +
    + {_rObj.color} +
    + ); + }; + + render(, container); + expect(ref.current.innerHTML).toEqual('blue'); + expect(ref.current.getAttribute('style')).toEqual('color: blue;'); + + rObj.color.set('red'); + expect(ref.current.getAttribute('style')).toEqual('color: red;'); + + expect(fn).toHaveBeenCalledTimes(1); + }); + + it('在input中使用响应式数据', () => { + let rObj; + const ref = createRef(); + const fn = jest.fn(); + + const App = () => { + const _rObj = useReactive({ class: 'c1', color: 'blue' }); + rObj = _rObj; + + fn(); + + return ; + }; + render(, container); + expect(ref.current.value).toEqual('blue'); + + rObj.color.set('red'); + expect(ref.current.value).toEqual('red'); + + expect(fn).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/inula-reactive/scripts/__tests__/ReactivityTest/reactive-watch.test.js b/packages/inula-reactive/scripts/__tests__/ReactivityTest/reactive-watch.test.js new file mode 100644 index 00000000..63e5d257 --- /dev/null +++ b/packages/inula-reactive/scripts/__tests__/ReactivityTest/reactive-watch.test.js @@ -0,0 +1,122 @@ +import Inula, {render, createRef, useReactive, useWatch, useCompute, For} from '../../../src/index'; + +describe('测试 watch', () => { + it('watch 一个参数', () => { + let rObj; + const ref = createRef(); + const fn = jest.fn(); + const App = () => { + const _rObj = useReactive(1); + useWatch(() => { + _rObj.get(); + fn(); + }); + + rObj = _rObj; + + return
    {_rObj}
    ; + }; + + render(, container); + rObj.set('2'); + expect(fn).toHaveBeenCalledTimes(2); + }); + + it('watch 2个参数', () => { + let rObj; + const ref = createRef(); + const fn = jest.fn(); + const App = () => { + const _rObj = useReactive(1); + useWatch(_rObj, () => { + fn(); + }); + rObj = _rObj; + + return
    {_rObj}
    ; + }; + + render(, container); + rObj.set('2'); + expect(fn).toHaveBeenCalledTimes(1); + }); + + it('watch 2个参数,第一个是函数', () => { + let rObj; + const ref = createRef(); + const fn = jest.fn(); + const App = () => { + const _rObj = useReactive(1); + useWatch( + () => { + _rObj.get(); + }, + () => { + fn(); + } + ); + rObj = _rObj; + + return
    {_rObj}
    ; + }; + + render(, container); + rObj.set('2'); + expect(fn).toHaveBeenCalledTimes(1); + }); + + it('响应式数据的孩子变更,watch也应该被触发', () => { + let rObj; + let ref = createRef(); + let fn = jest.fn(); + let appFn = jest.fn(); + let itemFn = jest.fn(); + + const App = () => { + const _rObj = useReactive([ + { id: 'id-1', name: 'p1' }, + { id: 'id-2', name: 'p2' }, + { id: 'id-3', name: 'p3' }, + ]); + rObj = _rObj; + + useWatch(() => { + _rObj.get(); + fn(); + }); + + appFn(); + + return ( +
    + + {item => { + itemFn(); + return ( +
  • + {item.name} +
  • + ); + }} +
    +
    + ); + }; + + render(, container); + let items = container.querySelectorAll('li'); + expect(items.length).toEqual(3); + expect(fn).toHaveBeenCalledTimes(1); + + rObj.push({ id: 'id-4', name: 'p4' }); + + items = container.querySelectorAll('li'); + expect(items.length).toEqual(4); + expect(fn).toHaveBeenCalledTimes(2); + + rObj[1].set({ id: 'id-2', name: 'p222' }); + let li = container.querySelector('#id-2'); + expect(li.innerHTML).toEqual('p222'); + expect(fn).toHaveBeenCalledTimes(3); + }); +}); diff --git a/packages/inula-reactive/scripts/__tests__/jest/commonComponents.js b/packages/inula-reactive/scripts/__tests__/jest/commonComponents.js new file mode 100644 index 00000000..1c077786 --- /dev/null +++ b/packages/inula-reactive/scripts/__tests__/jest/commonComponents.js @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import * as Inula from '../../../src/index'; +import { getLogUtils } from './testUtils'; + +export const App = props => { + const Parent = props.parent; + const Child = props.child; + + return ( +
    + + + +
    + ); +}; + +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/packages/inula-reactive/scripts/__tests__/jest/jestEnvironment.js b/packages/inula-reactive/scripts/__tests__/jest/jestEnvironment.js new file mode 100644 index 00000000..e62beb55 --- /dev/null +++ b/packages/inula-reactive/scripts/__tests__/jest/jestEnvironment.js @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +global.MessageChannel = function MessageChannel() { + this.port1 = {}; + this.port2 = { + // eslint-disable-next-line @typescript-eslint/no-empty-function + postMessage() {}, + }; +}; +global.__VERSION__ = require('../../../package.json').version; diff --git a/packages/inula-reactive/scripts/__tests__/jest/jestSetting.js b/packages/inula-reactive/scripts/__tests__/jest/jestSetting.js new file mode 100644 index 00000000..12eea3ba --- /dev/null +++ b/packages/inula-reactive/scripts/__tests__/jest/jestSetting.js @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { unmountComponentAtNode } from '../../../src/dom/DOMExternal'; +import { getLogUtils } from './testUtils'; + +const LogUtils = getLogUtils(); +global.isDev = process.env.NODE_ENV === 'development'; +global.isTest = true; +global.container = null; +global.beforeEach(() => { + LogUtils.clear(); + // 创建一个 DOM 元素作为渲染目标 + global.container = document.createElement('div'); + document.body.appendChild(global.container); +}); + +global.afterEach(() => { + unmountComponentAtNode(global.container); + global.container.remove(); + global.container = null; + LogUtils.clear(); +}); + +function runAssertion(fn) { + try { + fn(); + } catch (error) { + return { + pass: false, + message: () => error.message, + }; + } + return { pass: true }; +} + +function toMatchValue(LogUtils, expectedValues) { + return runAssertion(() => { + const actualValues = LogUtils.getAndClear(); + expect(actualValues).toEqual(expectedValues); + }); +} + +// 使Jest感知自定义匹配器 +expect.extend({ + toMatchValue, +}); diff --git a/packages/inula-reactive/scripts/__tests__/jest/logUtils.js b/packages/inula-reactive/scripts/__tests__/jest/logUtils.js new file mode 100644 index 00000000..ffd9096f --- /dev/null +++ b/packages/inula-reactive/scripts/__tests__/jest/logUtils.js @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +let dataArray = null; + +const log = value => { + if (dataArray === null) { + dataArray = [value]; + } else { + dataArray.push(value); + } +}; + +const getAndClear = () => { + if (dataArray === null) { + return []; + } + const values = dataArray; + dataArray = null; + return values; +}; + +const clear = () => { + dataArray = dataArray ? null : dataArray; +}; + +exports.clear = clear; +exports.log = log; +exports.getAndClear = getAndClear; diff --git a/packages/inula-reactive/scripts/__tests__/jest/testUtils.js b/packages/inula-reactive/scripts/__tests__/jest/testUtils.js new file mode 100644 index 00000000..3fa59da0 --- /dev/null +++ b/packages/inula-reactive/scripts/__tests__/jest/testUtils.js @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +export const stopBubbleOrCapture = (e, value) => { + const LogUtils = getLogUtils(); + LogUtils.log(value); + e.stopPropagation(); +}; + +export function triggerClickEvent(container, id) { + const event = new MouseEvent('click', { + bubbles: true, + }); + container.querySelector(`#${id}`).dispatchEvent(event); +} + +class LogUtils { + constructor() { + this.dataArray = null; + } + + log = value => { + if (this.dataArray === null) { + this.dataArray = [value]; + } else { + this.dataArray.push(value); + } + }; + + getAndClear = () => { + if (this.dataArray === null) { + return []; + } + const values = this.dataArray; + this.dataArray = null; + return values; + }; + + getNotClear = () => { + return this.dataArray === null ? [] : this.dataArray; + }; + + clear = () => { + this.dataArray = this.dataArray ? null : this.dataArray; + }; +} + +let logger; +export function getLogUtils() { + if (!logger) { + logger = new LogUtils(); + } + return logger; +} diff --git a/packages/inula-reactive/scripts/__tests__/utils/dispatchChangeEvent.js b/packages/inula-reactive/scripts/__tests__/utils/dispatchChangeEvent.js new file mode 100644 index 00000000..a0416d2c --- /dev/null +++ b/packages/inula-reactive/scripts/__tests__/utils/dispatchChangeEvent.js @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ +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 })); +} diff --git a/packages/inula-reactive/scripts/gen3rdLib.js b/packages/inula-reactive/scripts/gen3rdLib.js new file mode 100644 index 00000000..f3528b5e --- /dev/null +++ b/packages/inula-reactive/scripts/gen3rdLib.js @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +'use strict'; +const path = require('path'); +const fs = require('fs'); +const childProcess = require('child_process'); + +const inulaEcoPath = path.resolve(__dirname, '../../inula-ecosystem'); +if (!fs.existsSync(inulaEcoPath)) { + throw Error('inula-ecosystem not found, put inula-core and inula-ecosystem in same folder plz!'); +} + +const cmd = process.argv[2]; +childProcess.exec( + `npm run ${cmd}`, + { + cwd: inulaEcoPath, + }, + function (error, stdout) { + if (error) { + console.log(`Error: ${error}`); + } else { + console.log(`STDOUT: ${stdout}`); + } + } +); diff --git a/packages/inula-reactive/scripts/rollup/build-types.js b/packages/inula-reactive/scripts/rollup/build-types.js new file mode 100644 index 00000000..630cc9e4 --- /dev/null +++ b/packages/inula-reactive/scripts/rollup/build-types.js @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import fs from 'fs'; +import path from 'path'; +import dts from 'rollup-plugin-dts'; + +function deleteFolder(filePath) { + if (fs.existsSync(filePath)) { + if (fs.lstatSync(filePath).isDirectory()) { + const files = fs.readdirSync(filePath); + files.forEach(file => { + const nextFilePath = path.join(filePath, file); + const states = fs.lstatSync(nextFilePath); + if (states.isDirectory()) { + deleteFolder(nextFilePath); + } else { + fs.unlinkSync(nextFilePath); + } + }); + fs.rmdirSync(filePath); + } else if (fs.lstatSync(filePath).isFile()) { + fs.unlinkSync(filePath); + } + } +} + +/** + * 删除非空文件夹 + * @param folders {string[]} + * @returns {{buildEnd(): void, name: string}} + */ +export function cleanUp(folders) { + return { + name: 'clean-up', + buildEnd() { + folders.forEach(f => deleteFolder(f)); + }, + }; +} + +function buildTypeConfig() { + return { + input: ['./build/inula/@types/index.d.ts'], + output: { + file: './build/inula/@types/index.d.ts', + format: 'es', + }, + plugins: [dts(), cleanUp(['./build/inula/@types/'])], + }; +} + +export default [buildTypeConfig()]; diff --git a/packages/inula-reactive/scripts/rollup/copy-plugin.js b/packages/inula-reactive/scripts/rollup/copy-plugin.js new file mode 100644 index 00000000..78ff1e77 --- /dev/null +++ b/packages/inula-reactive/scripts/rollup/copy-plugin.js @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import fs from 'fs'; + +export default function copyFiles(copyPairs) { + return { + name: 'copy-files', + generateBundle() { + copyPairs.forEach(({ from, to }) => { + console.log(`copy files: ${from} → ${to}`); + fs.copyFileSync(from, to); + }); + }, + }; +} diff --git a/packages/inula-reactive/scripts/rollup/rollup.config.js b/packages/inula-reactive/scripts/rollup/rollup.config.js new file mode 100644 index 00000000..4ab91071 --- /dev/null +++ b/packages/inula-reactive/scripts/rollup/rollup.config.js @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +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 execute from 'rollup-plugin-execute'; +import { terser } from 'rollup-plugin-terser'; +import { version as inulaVersion } from '../../package.json'; + +const extensions = ['.js', '.ts', '.tsx']; + +const libDir = path.join(__dirname, '../..'); +const rootDir = path.join(__dirname, '../..'); +const outDir = path.join(rootDir, 'build', 'inula'); + +if (!fs.existsSync(path.join(rootDir, 'build'))) { + fs.mkdirSync(path.join(rootDir, 'build')); +} +if (!fs.existsSync(outDir)) { + fs.mkdirSync(outDir); +} + +const outputResolve = (...p) => path.resolve(outDir, ...p); + +const isDev = mode => { + return mode === 'development'; +}; + +const getBasicPlugins = mode => { + return [ + nodeResolve({ + extensions, + modulesOnly: true, + }), + babel({ + exclude: 'node_modules/**', + configFile: path.join(__dirname, '../../babel.config.js'), + babelHelpers: 'runtime', + extensions, + }), + replace({ + values: { + 'process.env.NODE_ENV': `"${mode}"`, + isDev: isDev(mode).toString(), + isTest: false, + __VERSION__: `"${inulaVersion}"`, + }, + preventAssignment: true, + }), + ]; +}; + +function getOutputName(mode) { + return mode === 'production' ? `inula.${mode}.min.js` : `inula.${mode}.js`; +} + +function genConfig(mode) { + const sourcemap = isDev(mode) ? 'inline' : false; + return { + input: path.resolve(libDir, 'src', 'index.ts'), + output: [ + { + file: outputResolve('cjs', getOutputName(mode)), + sourcemap, + format: 'cjs', + }, + { + file: outputResolve('umd', getOutputName(mode)), + sourcemap, + name: 'Inula', + format: 'umd', + }, + ], + plugins: [ + ...getBasicPlugins(mode), + execute('npm run build-types', true), + mode === 'production' && terser(), + copy([ + { + from: path.join(libDir, '/npm/index.js'), + to: path.join(outDir, 'index.js'), + }, + { + from: path.join(libDir, 'package.json'), + to: path.join(outDir, 'package.json'), + }, + ]), + ], + }; +} + +function genJSXRuntimeConfig(mode) { + return { + input: path.resolve(libDir, 'src', 'jsx-runtime.ts'), + output: { + file: outputResolve('jsx-runtime.js'), + format: 'cjs', + }, + plugins: [...getBasicPlugins(mode)], + }; +} + +function genJSXDEVRuntimeConfig(mode) { + return { + input: path.resolve(libDir, 'src', 'jsx-dev-runtime.ts'), + output: { + file: outputResolve('jsx-dev-runtime.js'), + format: 'cjs', + }, + plugins: [...getBasicPlugins(mode)], + }; +} + +export default [genConfig('development'), genConfig('production'), genJSXRuntimeConfig(''), genJSXDEVRuntimeConfig('')]; diff --git a/packages/inula-reactive/src/EventTypes.ts b/packages/inula-reactive/src/EventTypes.ts new file mode 100644 index 00000000..0746cb2e --- /dev/null +++ b/packages/inula-reactive/src/EventTypes.ts @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +export type DomAnimationEvent = AnimationEvent; +export type DomClipboardEvent = ClipboardEvent; +export type DomCompositionEvent = CompositionEvent; +export type DomDragEvent = DragEvent; +export type DomFocusEvent = FocusEvent; +export type DomKeyboardEvent = KeyboardEvent; +export type DomMouseEvent = MouseEvent; +export type DomTouchEvent = TouchEvent; +export type DomPointerEvent = PointerEvent; +export type DomTransitionEvent = TransitionEvent; +export type DomUIEvent = UIEvent; +export type DomWheelEvent = WheelEvent; diff --git a/packages/inula-reactive/src/dom/DOMExternal.ts b/packages/inula-reactive/src/dom/DOMExternal.ts new file mode 100644 index 00000000..001d18f5 --- /dev/null +++ b/packages/inula-reactive/src/dom/DOMExternal.ts @@ -0,0 +1,146 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +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 { listenSimulatedDelegatedEvents } from '../event/EventBinding'; +import { Callback } from '../renderer/Types'; +import { InulaNode } from '../types'; + +function createRoot(children: any, container: Container, callback?: Callback) { + // 清空容器 + let child = container.lastChild; + while (child) { + container.removeChild(child); + child = container.lastChild; + } + + // 调度器创建根节点,并给容器dom赋vNode结构体 + const treeRoot = createTreeRootVNode(container); + container._treeRoot = treeRoot; + listenSimulatedDelegatedEvents(treeRoot); + + // 执行回调 + if (typeof callback === 'function') { + const cb = callback; + callback = function () { + const instance = getFirstCustomDom(treeRoot); + cb.call(instance); + }; + } + + // 建VNode树,启动页面绘制 + syncUpdates(() => { + startUpdate(children, treeRoot, callback); + }); + + return treeRoot; +} + +function executeRender(children: any, container: Container, callback?: Callback) { + let treeRoot = container._treeRoot; + + if (!treeRoot) { + treeRoot = createRoot(children, container, callback); + } else { + // container被render过 + if (typeof callback === 'function') { + const cb = callback; + callback = function () { + const instance = getFirstCustomDom(treeRoot); + cb.call(instance); + }; + } + // 执行更新操作 + startUpdate(children, treeRoot, callback); + } + + return getFirstCustomDom(treeRoot); +} + +function findDOMNode(domOrEle?: Element): null | Element | Text { + if (domOrEle === null || domOrEle === undefined) { + return null; + } + + // 普通节点 + if (isElement(domOrEle)) { + return domOrEle; + } + + // class的实例 + 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: Element | DocumentFragment | Document): boolean; +function destroy(container: Container): boolean { + if (container._treeRoot) { + syncUpdates(() => { + executeRender(null, container, () => { + removeRootEventLister(container); + container._treeRoot = null; + }); + }); + + return true; + } + + return false; +} + +interface RootElement { + render(component: InulaNode): void; + + unmount(): void; +} + +function createRootElement(container: Container, option?: Record): RootElement { + return { + render(component: InulaNode) { + executeRender(component, container); + }, + unmount(): void { + destroy(container); + }, + }; +} + +export { + createPortal, + asyncUpdates as unstable_batchedUpdates, + findDOMNode, + executeRender as render, + createRootElement as createRoot, + destroy as unmountComponentAtNode, +}; diff --git a/packages/inula-reactive/src/dom/DOMInternalKeys.ts b/packages/inula-reactive/src/dom/DOMInternalKeys.ts new file mode 100644 index 00000000..a8727261 --- /dev/null +++ b/packages/inula-reactive/src/dom/DOMInternalKeys.ts @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +/** + * 文件整体功能:给dom节点赋 VNode 的结构体和事件初始化标记 + */ + +import type { VNode } from '../renderer/Types'; +import type { Container, Props } from './DOMOperator'; + +import { DomComponent, DomText, TreeRoot } from '../renderer/vnode/VNodeTags'; + +const INTERNAL_VNODE = '_inula_VNode'; +const INTERNAL_PROPS = '_inula_Props'; +const INTERNAL_NONDELEGATEEVENTS = '_inula_NonDelegatedEvents'; + +// 通过 VNode 实例获取 DOM 节点 +export function getDom(vNode: VNode): Element | Text | null { + const { tag } = vNode; + if (tag === DomComponent || tag === DomText) { + return vNode.realNode; + } + return null; +} + +// 将 VNode 属性相关信息挂到 DOM 对象的特定属性上 +export function saveVNode(vNode: VNode, dom: Element | Text | Container): void { + dom[INTERNAL_VNODE] = vNode; +} + +// 用 DOM 节点,来找其对应的 VNode 实例 +export function getVNode(dom: Node | Container): VNode | null { + const vNode = dom[INTERNAL_VNODE] || (dom as Container)._treeRoot; + if (vNode) { + const { tag } = vNode; + if (tag === DomComponent || tag === DomText || tag === TreeRoot) { + return vNode; + } + } + return null; +} + +// 用 DOM 对象,来寻找其对应或者说是最近父级的 vNode +export function getNearestVNode(dom: Node): null | VNode { + let domNode: Node | null = dom; + // 寻找当前节点及其所有祖先节点是否有标记VNODE + while (domNode) { + const vNode = domNode[INTERNAL_VNODE]; + if (vNode) { + return vNode; + } + domNode = domNode.parentNode; + } + + return null; +} + +// 获取 vNode 上的属性相关信息 +export function getVNodeProps(dom: Element | Text): Props | null { + return dom[INTERNAL_PROPS] || null; +} + +// 将 DOM 属性相关信息挂到 DOM 对象的特定属性上 +export function updateVNodeProps(dom: Element | Text, props: Props): void { + dom[INTERNAL_PROPS] = props; +} + +export function getNonDelegatedListenerMap(dom: Element | Text): Map { + let eventsMap = dom[INTERNAL_NONDELEGATEEVENTS]; + if (!eventsMap) { + eventsMap = new Map(); + dom[INTERNAL_NONDELEGATEEVENTS] = eventsMap; + } + return eventsMap; +} diff --git a/packages/inula-reactive/src/dom/DOMOperator.ts b/packages/inula-reactive/src/dom/DOMOperator.ts new file mode 100644 index 00000000..56eef88f --- /dev/null +++ b/packages/inula-reactive/src/dom/DOMOperator.ts @@ -0,0 +1,224 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { saveVNode, updateVNodeProps } from './DOMInternalKeys'; +import { createDom } from './utils/DomCreator'; +import { getSelectionInfo, resetSelectionRange, SelectionData } from './SelectionRangeHandler'; +import { isDocument, shouldAutoFocus } from './utils/Common'; +import { NSS } from './utils/DomCreator'; +import { adjustStyleValue } from './DOMPropertiesHandler/StyleHandler'; +import type { VNode } from '../renderer/Types'; +import { setInitValue, getPropsWithoutValue, updateValue } from './valueHandler'; +import { compareProps, setDomProps } from './DOMPropertiesHandler/DOMPropertiesHandler'; +import { isNativeElement, validateProps } from './validators/ValidateProps'; +import { watchValueChange } from './valueHandler/ValueChangeHandler'; +import { DomComponent, DomText } from '../renderer/vnode/VNodeTags'; +import { updateCommonProp } from './DOMPropertiesHandler/UpdateCommonProp'; +import { getCurrentRoot } from '../renderer/RootStack'; + +export type Props = Record & { + autoFocus?: boolean; + children?: any; + dangerouslySetInnerHTML?: any; + disabled?: boolean; + hidden?: boolean; + style?: { display?: string }; +}; + +export type Container = (Element & { _treeRoot?: VNode | null }) | (Document & { _treeRoot?: VNode | null }); + +let selectionInfo: null | SelectionData = null; + +function getChildNS(parentNS: string | null, tagName: string): string { + if (parentNS === NSS.svg && tagName === 'foreignObject') { + return NSS.html; + } + + if (parentNS === null || parentNS === NSS.html) { + // 没有父命名空间,或父命名空间为xhtml + return NSS[tagName] ?? NSS.html; + } + + // 默认返回parentNamespace. + return parentNS; +} + +// 获取容器 +export function getNSCtx(parentNS: string, type: string, dom?: Container): string { + return dom ? getChildNS(dom.namespaceURI ?? null, dom.nodeName) : getChildNS(parentNS, type); +} + +export function prepareForSubmit(): void { + selectionInfo = getSelectionInfo(); +} + +export function resetAfterSubmit(): void { + resetSelectionRange(selectionInfo); + selectionInfo = null; +} + +// 创建 DOM 对象 +export function newDom(tagName: string, props: Props, parentNamespace: string, vNode: VNode): Element { + // document取值于treeRoot对应的DOM的ownerDocument。 + // 解决:在iframe中使用top的inula时,inula在创建DOM时用到的document并不是iframe的document,而是top中的document的问题。 + const rootDom = getCurrentRoot()?.realNode; + const doc = isDocument(rootDom) ? rootDom : rootDom.ownerDocument; + + const dom: Element = createDom(tagName, parentNamespace, doc); + // 将 vNode 节点挂到 DOM 对象上 + saveVNode(vNode, dom); + // 将属性挂到 DOM 对象上 + updateVNodeProps(dom, props); + + return dom; +} + +// 设置节点默认事件、属性 +export function initDomProps(dom: Element, tagName: string, rawProps: Props): boolean { + validateProps(tagName, rawProps); + + // 获取不包括value,defaultValue的属性 + const props: Record = getPropsWithoutValue(tagName, dom, rawProps); + + // 初始化DOM属性(不包括value,defaultValue) + const isNativeTag = isNativeElement(tagName, props); + setDomProps(dom, props, isNativeTag, true); + + if (tagName === 'input' || tagName === 'textarea') { + // 增加监听value和checked的set、get方法 + watchValueChange(dom); + } + + // 设置dom.value值,触发受控组件的set方法 + setInitValue(tagName, dom, rawProps); + + return shouldAutoFocus(tagName, rawProps); +} + +// 准备更新之前进行一系列校验 DOM,寻找属性差异等准备工作 +export function getPropChangeList( + dom: Element, + type: string, + lastRawProps: Props, + nextRawProps: Props +): Record { + // 校验两个对象的不同 + validateProps(type, nextRawProps); + + // 重新定义的属性不需要参与对比,被代理的组件需要把这些属性覆盖到props中 + const oldProps: Record = getPropsWithoutValue(type, dom, lastRawProps); + const newProps: Record = getPropsWithoutValue(type, dom, nextRawProps); + + return compareProps(oldProps, newProps); +} + +export function isTextChild(type: string, props: Props): boolean { + if (type === 'textarea' || type === 'option' || type === 'noscript') { + return true; + } + const childType = typeof props.children; + if (childType === 'string' || childType === 'number') { + return true; + } else { + return ( + props.dangerouslySetInnerHTML && + typeof props.dangerouslySetInnerHTML === 'object' && + props.dangerouslySetInnerHTML.__html !== null && + props.dangerouslySetInnerHTML.__html !== undefined + ); + } +} + +export function newTextDom(text: string, processing: VNode): Text { + const textNode: Text = document.createTextNode(text); + saveVNode(processing, textNode); + return textNode; +} + +// 提交vNode的类型为DomComponent或者DomText的更新 +export function submitDomUpdate(tag: string, vNode: VNode) { + const newProps = vNode.props; + const element: Element | null = vNode.realNode; + + if (tag === DomComponent) { + // DomComponent类型 + if (element !== null && element !== undefined) { + const type = vNode.type; + const changeList = vNode.changeList; + vNode.changeList = null; + + if (changeList !== null) { + saveVNode(vNode, element); + updateVNodeProps(element, newProps); + // 应用diff更新Properties. + // 当一个选中的radio改变名称,浏览器使另一个radio的复选框为false. + if ( + type === 'input' && + newProps.type === 'radio' && + newProps.name !== null && + newProps.name !== undefined && + newProps.checked !== null && + newProps.checked !== undefined + ) { + updateCommonProp(element, 'checked', newProps.checked, true); + } + const isNativeTag = isNativeElement(type, newProps); + setDomProps(element, changeList, isNativeTag, false); + updateValue(type, element, newProps); + } + } + } else if (tag === DomText) { + if (element != null) { + // text类型 + element.textContent = newProps; + } + } +} + +export function clearText(dom: Element): void { + dom.textContent = ''; +} + +// 添加child元素 +export function appendChildElement(parent: Element | Container, child: Element | Text): void { + parent.appendChild(child); +} + +// 插入dom元素 +export function insertDomBefore(parent: Element | Container, child: Element | Text, beforeChild: Element | Text) { + parent.insertBefore(child, beforeChild); +} + +export function removeChildDom(parent: Element | Container, child: Element | Text) { + parent.removeChild(child); +} + +// 隐藏元素 +export function hideDom(tag: string, dom: Element | Text) { + if (tag === DomComponent) { + (dom as HTMLElement).style.display = 'none'; + } else if (tag === DomText) { + dom.textContent = ''; + } +} + +// 不隐藏元素 +export function unHideDom(tag: string, dom: Element | Text, props?: Props) { + if (tag === DomComponent) { + (dom as HTMLElement).style.display = adjustStyleValue('display', props?.style?.display ?? ''); + } else if (tag === DomText) { + dom.textContent = props as any; + } +} diff --git a/packages/inula-reactive/src/dom/DOMPropertiesHandler/DOMPropertiesHandler.ts b/packages/inula-reactive/src/dom/DOMPropertiesHandler/DOMPropertiesHandler.ts new file mode 100644 index 00000000..7bedfb83 --- /dev/null +++ b/packages/inula-reactive/src/dom/DOMPropertiesHandler/DOMPropertiesHandler.ts @@ -0,0 +1,202 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { allDelegatedInulaEvents } from '../../event/EventHub'; +import { updateCommonProp } from './UpdateCommonProp'; +import { setStyles } from './StyleHandler'; +import { lazyDelegateOnRoot, listenNonDelegatedEvent } from '../../event/EventBinding'; +import { isEventProp } from '../validators/ValidateProps'; +import { getCurrentRoot } from '../../renderer/RootStack'; +import { getValue, isReactiveObj} from '../../reactive/Utils'; +import { handleReactiveProp } from '../../reactive/RContextCreator'; +import { ReactiveProxy } from '../../reactive/types'; + +export function unwrapVal(propName: string, propVal: any, dom: Element, styleName?: string) { + const rawVal: any = handleReactiveProp(dom, propName, propVal, styleName); + return rawVal; +} + +// 初始化DOM属性和更新 DOM 属性 +export function setDomProps(dom: Element, props: Record, isNativeTag: boolean, isInit: boolean): void { + const keysOfProps = Object.keys(props); + let propName; + let propVal; + const keyLength = keysOfProps.length; + for (let i = 0; i < keyLength; i++) { + propName = keysOfProps[i]; + propVal = props[propName]; + + if (propName === 'style') { + setStyles(dom, propVal); + } else if (isEventProp(propName)) { + // 事件监听属性处理 + const currentRoot = getCurrentRoot(); + if (!allDelegatedInulaEvents.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; + if (type === 'string' || type === 'number' || isReactiveObj(propVal)) { + dom.textContent = unwrapVal(propName, propVal, dom); + } + } else if (propName === 'dangerouslySetInnerHTML') { + dom.innerHTML = propVal.__html; + } else if (!isInit || (propVal !== null && propVal !== undefined)) { + updateCommonProp(dom, propName, propVal, isNativeTag); + } + } +} + +// 找出两个 DOM 属性的差别,生成需要更新的属性集合 +export function compareProps(oldProps: Record, newProps: Record, dom: Element): Record { + let updatesForStyle = {}; + const toUpdateProps: Record = {}; + const keysOfOldProps = Object.keys(oldProps); + const keysOfNewProps = Object.keys(newProps); + + const oldPropsLength = keysOfOldProps.length; + let propName; + let oldStyle; + let styleProps; + let styleProp; + // 找到旧属性中需要删除的属性 + for (let i = 0; i < oldPropsLength; i++) { + propName = keysOfOldProps[i]; + // 新属性中包含该属性或者该属性为空值的属性不需要处理 + if (oldProps[propName] === null || oldProps[propName] === undefined || keysOfNewProps.includes(propName)) { + continue; + } + + if (propName === 'style') { + oldStyle = oldProps[propName]; + styleProps = Object.keys(oldStyle); + for (let j = 0; j < styleProps.length; j++) { + styleProp = styleProps[j]; + updatesForStyle[styleProp] = ''; + } + } else if (propName === 'autoFocus' || propName === 'children' || propName === 'dangerouslySetInnerHTML') { + continue; + } else if (isEventProp(propName)) { + if (!allDelegatedInulaEvents.has(propName)) { + toUpdateProps[propName] = null; + } + } else { + // 其它属性都要加入到删除队列里面,等待删除 + toUpdateProps[propName] = null; + } + } + + let newPropValue; + let oldPropValue; + let oldStyleProps; + let newStyleProps; + let newHTML; + let oldHTML; + // 遍历新属性,获取新增和变更属性 + for (let i = 0; i < keysOfNewProps.length; i++) { + propName = keysOfNewProps[i]; + newPropValue = newProps[propName]; + oldPropValue = oldProps != null ? oldProps[propName] : null; + + if ( + newPropValue === oldPropValue || + (newPropValue == null && oldPropValue == null) || + (isReactiveObj(newPropValue) && + isReactiveObj(oldPropValue) && + getValue(newPropValue) === getValue(oldPropValue)) + ) { + // 新旧属性值未发生变化,或者新旧属性皆为空值,不需要进行处理 + continue; + } + + if (propName === 'style') { + if (oldPropValue) { + // 之前 style 属性有设置非空值 + // 原来有这个 style,但现在没这个 style 了 + oldStyleProps = Object.keys(oldPropValue); + for (let j = 0; j < oldStyleProps.length; j++) { + styleProp = oldStyleProps[j]; + if (!newPropValue || !Object.prototype.hasOwnProperty.call(newPropValue, styleProp)) { + updatesForStyle[styleProp] = ''; + } + } + + // 现在有这个 style,但是和原来不相等 + newStyleProps = newPropValue ? Object.keys(newPropValue) : []; + for (let j = 0; j < newStyleProps.length; j++) { + styleProp = newStyleProps[j]; + if (oldPropValue[styleProp] !== newPropValue[styleProp]) { + updatesForStyle[styleProp] = newPropValue[styleProp]; + } + } + } else { + // 之前未设置 style 属性或者设置了空值 + if (Object.keys(updatesForStyle).length === 0) { + toUpdateProps[propName] = null; + } + updatesForStyle = newPropValue; + } + } else if (propName === 'dangerouslySetInnerHTML') { + newHTML = newPropValue ? newPropValue.__html : undefined; + oldHTML = oldPropValue ? oldPropValue.__html : undefined; + if (newHTML != null) { + if (oldHTML !== newHTML) { + appendToUpdateProps(toUpdateProps, propName, newPropValue, dom); + } + } + } else if (propName === 'children') { + if (typeof newPropValue === 'string' || typeof newPropValue === 'number' || isReactiveObj(newPropValue)) { + appendToUpdateProps, string>( + toUpdateProps, + propName, + newPropValue, + dom, + String + ); + } + } else if (isEventProp(propName)) { + const currentRoot = getCurrentRoot(); + if (!allDelegatedInulaEvents.has(propName)) { + appendToUpdateProps(toUpdateProps, propName, newPropValue, dom); + } else if (currentRoot && !currentRoot.delegatedEvents.has(propName)) { + lazyDelegateOnRoot(currentRoot, propName); + } + } else { + appendToUpdateProps(toUpdateProps, propName, newPropValue, dom); + } + } + + // 处理style + if (Object.keys(updatesForStyle).length > 0) { + appendToUpdateProps(toUpdateProps, 'style', updatesForStyle, dom); + } + + return toUpdateProps; +} + +function appendToUpdateProps( + toUpdateProps: Record, + propName: string, + propVal: V, + dom: Element, + formatter?: (value: V) => R +) { + const rawVal: any = handleReactiveProp(dom, propName, propVal); + + toUpdateProps[propName] = formatter ? formatter(rawVal) : rawVal; +} diff --git a/packages/inula-reactive/src/dom/DOMPropertiesHandler/StyleHandler.ts b/packages/inula-reactive/src/dom/DOMPropertiesHandler/StyleHandler.ts new file mode 100644 index 00000000..ae8bc0fc --- /dev/null +++ b/packages/inula-reactive/src/dom/DOMPropertiesHandler/StyleHandler.ts @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { unwrapVal } from './DOMPropertiesHandler'; + +/** + * 不需要加长度单位的 css 属性 + */ +const noUnitCSS = [ + 'animationIterationCount', + 'columnCount', + 'columns', + 'gridArea', + 'fontWeight', + 'lineClamp', + 'lineHeight', + 'opacity', + 'order', + 'orphans', + 'tabSize', + 'widows', + 'zIndex', + 'zoom', +]; + +const length = noUnitCSS.length; +for (let i = 0; i < length; i++) { + const cssKey = noUnitCSS[i]; + const attributeKey = cssKey.charAt(0).toUpperCase() + cssKey.slice(1); + + // css 兼容性前缀 webkit: chrome, mo: IE或者Edge, Moz: 火狐 + noUnitCSS.push('Webkit' + attributeKey); + noUnitCSS.push('mo' + attributeKey); + noUnitCSS.push('Moz' + attributeKey); +} + +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') + ); +} + +/** + * 对一些没有写单位的样式进行适配,例如:width: 10 => width: 10px + * 对空值或布尔值进行适配,转为空字符串 + * 去掉多余空字符 + */ +export function adjustStyleValue(name, value) { + let validValue = value; + + if (typeof value === 'number' && value !== 0 && isNeedUnitCSS(name)) { + validValue = `${value}px`; + } else if (value === '' || value === null || value === undefined || typeof value === 'boolean') { + validValue = ''; + } + + return validValue; +} + +/** + * 设置 DOM 节点的 style 属性 + */ +export function setStyles(dom, styles) { + if (!styles) { + return; + } + + const style = dom.style; + Object.keys(styles).forEach(name => { + const styleVal = styles[name]; + const val = unwrapVal('style', styleVal, dom, name); + + // 以--开始的样式直接设置即可 + if (name.indexOf('--') === 0) { + style.setProperty(name, val); + } else { + // 使用这种赋值方式,浏览器可以将'WebkitLineClamp', 'backgroundColor'分别识别为'-webkit-line-clamp'和'backgroud-color' + style[name] = adjustStyleValue(name, val); + } + }); +} diff --git a/packages/inula-reactive/src/dom/DOMPropertiesHandler/UpdateCommonProp.ts b/packages/inula-reactive/src/dom/DOMPropertiesHandler/UpdateCommonProp.ts new file mode 100644 index 00000000..27cc30e0 --- /dev/null +++ b/packages/inula-reactive/src/dom/DOMPropertiesHandler/UpdateCommonProp.ts @@ -0,0 +1,155 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { getPropDetails, PROPERTY_TYPE } from '../validators/PropertiesData'; +import { isInvalidValue } from '../validators/ValidateProps'; +import { getNamespaceCtx } from '../../renderer/ContextSaver'; +import { NSS } from '../utils/DomCreator'; +import { getDomTag } from '../utils/Common'; +import { unwrapVal } from './DOMPropertiesHandler'; + +// 不需要装换的svg属性集合 +const svgHumpAttr = new Set(); +[ + 'allowReorder', + 'autoReverse', + 'baseFrequency', + 'baseProfile', + 'calcMode', + 'clipPathUnits', + 'contentScriptType', + 'contentStyleType', + 'diffuseConstant', + 'edgeMode', + 'externalResourcesRequired', + 'filterRes', + 'filterUnits', + 'glyphRef', + 'gradientTransform', + 'gradientUnits', + 'kernelMatrix', + 'kernelUnitLength', + 'keyPoints', + 'keySplines', + 'keyTimes', + 'lengthAdjust', + 'limitingConeAngle', + 'markerHeight', + 'markerUnits', + 'markerWidth', + 'maskContentUnits', + 'maskUnits', + 'numOctaves', + 'pathLength', + 'patternContentUnits', + 'patternTransform,', + 'patternUnits', + 'pointsAtX', + 'pointsAtY', + 'pointsAtZ', + 'preserveAlpha', + 'preserveAspectRatio', + 'primitiveUnits', + 'referrerPolicy', + 'refX', + 'refY', + 'repeatCount', + 'repeatDur', + 'requiredExtensions', + 'requiredFeatures', + 'specularConstant', + 'specularExponent', + 'spreadMethod', + 'startOffset', + 'stdDeviation', + 'stitchTiles', + 'surfaceScale', + 'systemLanguage', + 'tableValues', + 'targetX', + 'targetY', + 'textLength', + 'viewBox', + 'viewTarget', + 'xChannelSelector', + 'yChannelSelector', + 'zoomAndPan', +].forEach(name => svgHumpAttr.add(name)); + +// 驼峰 变 “-” +function convertToLowerCase(str) { + const replacer = (match, char) => `-${char.toLowerCase()}`; + return str.replace(/([A-Z])/g, replacer); +} + +/** + * 给 dom 设置属性 + * attrName 指代码中属性设置的属性名称(如 class) + * 多数情况 attrName 仅用作初始 DOM 节点对象使用,而 property 更多用于页面交互 + */ +export function updateCommonProp(dom: Element, attrName: string, value: any, isNativeTag: boolean) { + const propDetails = getPropDetails(attrName); + + if (isInvalidValue(attrName, value, propDetails, isNativeTag)) { + value = null; + } + + if (attrName === '') { + return; + } + + if (!isNativeTag || propDetails === null) { + // 特殊处理svg的属性,把驼峰式的属性名称转成'-' + if (getDomTag(dom) === 'svg' || getNamespaceCtx() === NSS.svg) { + if (!svgHumpAttr.has(attrName)) { + attrName = convertToLowerCase(attrName); + } + } + + if (value === null) { + dom.removeAttribute(attrName); + } else { + dom.setAttribute(attrName, String(unwrapVal(attrName, value, dom))); + } + } else if (['checked', 'multiple', 'muted', 'selected'].includes(propDetails.attrName)) { + if (value === null) { + // 必填属性设置默认值 + dom[propDetails.attrName] = false; + } else { + dom[propDetails.attrName] = value; + } + } else { + // 处理其他普通属性 + if (value === null) { + dom.removeAttribute(propDetails.attrName); + } else { + const { type, attrNS } = propDetails; // 数据类型、固有属性命名空间 + const attributeName = propDetails.attrName; // 固有属性名 + let attributeValue; + if (type === PROPERTY_TYPE.BOOLEAN) { + // 即可以用作标志又可以是属性值的属性 + attributeValue = ''; + } else { + attributeValue = String(unwrapVal(attrName, value, dom)); + } + + if (attrNS) { + dom.setAttributeNS(attrNS, attributeName, attributeValue); + } else { + dom.setAttribute(attributeName, attributeValue); + } + } + } +} diff --git a/packages/inula-reactive/src/dom/SelectionRangeHandler.ts b/packages/inula-reactive/src/dom/SelectionRangeHandler.ts new file mode 100644 index 00000000..e7769821 --- /dev/null +++ b/packages/inula-reactive/src/dom/SelectionRangeHandler.ts @@ -0,0 +1,184 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +/** + * 处理文本框、输入框中框选范围内的数据 + */ + +import { getIFrameFocusedDom, isText } from './utils/Common'; +import { isElement } from './utils/Common'; + +type SelectionRange = { + start: number | null, + end: number | null +} + +/** + * 设置聚焦的 textarea 或 input 节点的选择范围 + * @param dom 需要设置选择范围的 input 或 textarea + * @param range 选择范围对象 + */ +function setSelectionRange(dom: HTMLInputElement | HTMLTextAreaElement, range) { + const { start, end } = range; + let realEnd = end; + + if (realEnd === null || realEnd === undefined) { + realEnd = start; + } + + if (typeof dom.setSelectionRange === 'function') { + dom.setSelectionRange(start, realEnd); + } +} + +/** + * 获取文本框、输入框中选中的文本的范围 + * @param dom 需要设置选择范围的 input 或 textarea + * @return {start: selectionStart, end: selectionEnd} + */ +function getSelectionRange(dom: Element | HTMLInputElement | HTMLTextAreaElement | void | null) { + const selectionRange: SelectionRange = { start: 0, end: 0 }; + + if (!dom) { + return selectionRange; + } + if ('selectionStart' in dom) { + // 现代浏览器的 input 或 textarea 有 selectionStart 属性. + selectionRange.start = dom.selectionStart; + selectionRange.end = dom.selectionEnd; + } + + return selectionRange; +} + +// 判断第一个节点和另一个节点是否是包含关系 +function isNodeContainsByTargetNode(targetNode, node) { + if (!targetNode || !node) { + return false; + } + if (targetNode === node) { + return true; + } + if (isText(targetNode)) { + return false; + } + if (isText(node)) { + return isNodeContainsByTargetNode(targetNode, node.parentNode); + } + if (typeof targetNode.contains === 'function') { + return targetNode.contains(node); // 该的节点是否为目标节点的后代节点 + } + if (typeof targetNode.compareDocumentPosition === 'function') { + // compareDocumentPosition 数值,表示两个节点彼此做比较的位置 + const CONTAINS_CODE = 16; + // 返回 16 代表 第二节点在第一节点内部 + return targetNode.compareDocumentPosition(node) === CONTAINS_CODE; + } + return false; +} + +function isInDocument(dom) { + if (dom && dom.ownerDocument) { + return isNodeContainsByTargetNode(dom.ownerDocument.documentElement, dom); + } + return false; +} + +// 判断一个标签是否有设置选择范围的能力 +export function hasSelectionProperties(dom) { + let elementType; + + if (dom && dom.nodeName) { + elementType = dom.nodeName.toLowerCase(); + const validInputType = ['text', 'search', 'tel', 'url', 'password']; + + if (elementType === 'input') { + return validInputType.includes(dom.type); + } else if (elementType === 'textarea') { + return dom.contentEditable === 'true'; + } else { + return false; + } + } else { + return false; + } +} + +// 返回当前 focus 的元素以及其选中的范围 +export function getSelectionInfo() { + const focusedDom = getIFrameFocusedDom(); + return { + focusedDom, + selectionRange: hasSelectionProperties(focusedDom) ? getSelectionRange(focusedDom) : null, + }; +} + +export interface SelectionData { + focusedDom: HTMLInputElement | HTMLTextAreaElement | void; + selectionRange?: { + start: number; + end: number; + }; +} + +// 防止选择范围内的信息因为节点删除或其他原因导致的信息丢失 +export function resetSelectionRange(preSelectionRangeData: SelectionData | null) { + // 当前 focus 的元素 + const currentFocusedDom = getIFrameFocusedDom(); + + // 先前 focus 的元素 + const preFocusedDom = preSelectionRangeData?.focusedDom; + + if (!preFocusedDom) { + return; + } + + // 先前的选择范围信息 + const preSelectionRange = preSelectionRangeData?.selectionRange; + + if (currentFocusedDom !== preFocusedDom && isInDocument(preFocusedDom)) { + if (preSelectionRange !== null) { + setSelectionRange(preFocusedDom, preSelectionRange); + } + + // 滚动条位置可能会因为一个节点的选中变化位置,需要做处理 + const ancestors: Record[] = []; + let ancestor: any = preFocusedDom.parentNode; + // 查找先前的 focus 节点的先祖 + while (ancestor) { + if (isElement(ancestor)) { + // 是元素节点,就把先祖信息放到先祖数组中 + const { scrollLeft, scrollTop } = ancestor; + ancestors.push({ + dom: ancestor, + scrollLeft, + scrollTop, + }); + } + ancestor = ancestor.parentNode; + } + + // 执行先前 focus 节点的 focus 方法 + if (typeof preFocusedDom.focus === 'function') { + preFocusedDom.focus(); + } + + ancestors.forEach(ancestorInfo => { + const ancestorDom = ancestorInfo.dom; + ancestorDom.scrollLeft = ancestorInfo.scrollLeft; + ancestorDom.scrollTop = ancestorInfo.scrollTop; + }); + } +} diff --git a/packages/inula-reactive/src/dom/utils/Common.ts b/packages/inula-reactive/src/dom/utils/Common.ts new file mode 100644 index 00000000..eb096f8d --- /dev/null +++ b/packages/inula-reactive/src/dom/utils/Common.ts @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { InulaDom } from './Interface'; +import { Props } from '../DOMOperator'; + +/** + * 获取当前聚焦的 input 或者 textarea 元素 + * @param doc 指定 document + */ +export function getFocusedDom(doc?: Document): InulaDom | null { + const currentDocument = doc ?? document; + + return currentDocument.activeElement ?? currentDocument.body; +} + +// 如果 input 或者 textarea 元素中有文字被选中时,activeElement 属性就会返回该元素 +// 此处是为了返回深层的 iframe 中的真实元素 +export function getIFrameFocusedDom() { + const currentWindow = window; + let focusedDom = getFocusedDom(); + // 深度优先,返回的元素如果是 iframe 对象则继续查找 + while (focusedDom instanceof currentWindow.HTMLIFrameElement) { + try { + // 访问 HTMLIframeElement 的 contentDocument 可能会导致浏览器抛出错误 + if (typeof focusedDom.contentWindow?.location.href === 'string') { + // iframe 的内容为同源 + focusedDom = getFocusedDom(focusedDom.contentWindow.document); + } else { + // 非同源 iframe 因为安全性原因无法获取其中的具体元素 + break; + } + } catch (e) { + // 非同源 iframe 因为安全性原因无法获取其中的具体元素 + break; + } + } + return focusedDom; +} + +export function isElement(dom) { + return dom.nodeType === 1; +} + +export function isText(dom) { + return dom.nodeType === 3; +} + +export function isComment(dom) { + return dom.nodeType === 8; +} + +export function isDocument(dom) { + return dom.nodeType === 9; +} + +export function isDocumentFragment(dom) { + return dom.nodeType === 11; +} + +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 +export function shouldAutoFocus(tagName: string, props: Props): boolean { + return types.includes(tagName) ? Boolean(props.autoFocus) : false; +} + +export function isNotNull(object: any): boolean { + return object !== null && object !== undefined; +} diff --git a/packages/inula-reactive/src/dom/utils/DomCreator.ts b/packages/inula-reactive/src/dom/utils/DomCreator.ts new file mode 100644 index 00000000..41b36da3 --- /dev/null +++ b/packages/inula-reactive/src/dom/utils/DomCreator.ts @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +export const NSS = { + html: 'http://www.w3.org/1999/xhtml', + math: 'http://www.w3.org/1998/Math/MathML', + svg: 'http://www.w3.org/2000/svg', +}; + +// 创建DOM元素 +export function createDom(tagName: string, parentNamespace: string, doc: Document): Element { + let dom: Element; + const selfNamespace = NSS[tagName] || NSS.html; + const ns = parentNamespace !== NSS.html ? parentNamespace : selfNamespace; + + if (ns !== NSS.html) { + dom = doc.createElementNS(ns, tagName); + } else { + dom = doc.createElement(tagName); + } + return dom; +} diff --git a/packages/inula-reactive/src/dom/utils/Interface.ts b/packages/inula-reactive/src/dom/utils/Interface.ts new file mode 100644 index 00000000..5d3e6eaa --- /dev/null +++ b/packages/inula-reactive/src/dom/utils/Interface.ts @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +export interface Props { + [propName: string]: any; +} + +export interface InulaSelect extends HTMLSelectElement { + _multiple?: boolean; +} + +export type InulaDom = Element | HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement; diff --git a/packages/inula-reactive/src/dom/validators/PropertiesData.ts b/packages/inula-reactive/src/dom/validators/PropertiesData.ts new file mode 100644 index 00000000..ac47f517 --- /dev/null +++ b/packages/inula-reactive/src/dom/validators/PropertiesData.ts @@ -0,0 +1,142 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +/* eslint-disable no-sparse-arrays */ + +// 属性值的数据类型 +export enum PROPERTY_TYPE { + BOOLEAN, // 普通布尔类型 + STRING, // 普通的字符串类型 + SPECIAL, // 需要特殊处理的属性类型 + BOOLEAN_STR, // 字符串类型的 true false +} + +export type PropDetails = { + propName: string; + type: PROPERTY_TYPE; + attrName: string; + attrNS: string | null; +}; + +type PropData = string | PROPERTY_TYPE | undefined | null; + +// 属性相关数据 +// 依次为 propertyName、type、attributeName、attributeNamespace,不填则使用默认值 +// type 默认 STRING +// attributeName 默认与 propertyName 相同 +// attributeNamespace 默认 null +const propertiesData = [ + // 一些特殊属性 + ['children', PROPERTY_TYPE.SPECIAL], + ['dangerouslySetInnerHTML', PROPERTY_TYPE.SPECIAL], + ['defaultValue', PROPERTY_TYPE.SPECIAL], + ['defaultChecked', PROPERTY_TYPE.SPECIAL], + ['innerHTML', PROPERTY_TYPE.SPECIAL], + ['style', PROPERTY_TYPE.SPECIAL], + + // propertyName 和 attributeName 不一样 + ['acceptCharset', , 'accept-charset'], + ['className', , 'class'], + ['htmlFor', , 'for'], + ['httpEquiv', , 'http-equiv'], + // 字符串类型的 true false + ['contentEditable', PROPERTY_TYPE.BOOLEAN_STR, 'contenteditable'], + ['spellCheck', PROPERTY_TYPE.BOOLEAN_STR, 'spellcheck'], + ['draggable', PROPERTY_TYPE.BOOLEAN_STR], + ['value', PROPERTY_TYPE.BOOLEAN_STR], + // SVG 相关,字符串类型的 true false + ['autoReverse', PROPERTY_TYPE.BOOLEAN_STR], + ['externalResourcesRequired', PROPERTY_TYPE.BOOLEAN_STR], + ['focusable', PROPERTY_TYPE.BOOLEAN_STR], + ['preserveAlpha', PROPERTY_TYPE.BOOLEAN_STR], + // 布尔类型 + ['allowFullScreen', PROPERTY_TYPE.BOOLEAN, 'allowfullscreen'], + ['async', PROPERTY_TYPE.BOOLEAN], + ['autoFocus', PROPERTY_TYPE.BOOLEAN, 'autofocus'], + ['autoPlay', PROPERTY_TYPE.BOOLEAN, 'autoplay'], + ['controls', PROPERTY_TYPE.BOOLEAN], + ['default', PROPERTY_TYPE.BOOLEAN], + ['defer', PROPERTY_TYPE.BOOLEAN], + ['disabled', PROPERTY_TYPE.BOOLEAN], + ['disablePictureInPicture', PROPERTY_TYPE.BOOLEAN, 'disablepictureinpicture'], + ['disableRemotePlayback', PROPERTY_TYPE.BOOLEAN, 'disableremoteplayback'], + ['formNoValidate', PROPERTY_TYPE.BOOLEAN, 'formnovalidate'], + ['hidden', PROPERTY_TYPE.BOOLEAN], + ['loop', PROPERTY_TYPE.BOOLEAN], + ['noModule', PROPERTY_TYPE.BOOLEAN, 'nomodule'], + ['noValidate', PROPERTY_TYPE.BOOLEAN, 'novalidate'], + ['open', PROPERTY_TYPE.BOOLEAN], + ['playsInline', PROPERTY_TYPE.BOOLEAN, 'playsinline'], + ['readOnly', PROPERTY_TYPE.BOOLEAN, 'readonly'], + ['required', PROPERTY_TYPE.BOOLEAN], + ['reversed', PROPERTY_TYPE.BOOLEAN], + ['scoped', PROPERTY_TYPE.BOOLEAN], + ['seamless', PROPERTY_TYPE.BOOLEAN], + ['itemScope', PROPERTY_TYPE.BOOLEAN, 'itemscope'], + // 框架需要当做 property 来处理的,而不是 attribute 来处理的属性 + ['checked', PROPERTY_TYPE.BOOLEAN], + ['multiple', PROPERTY_TYPE.BOOLEAN], + ['muted', PROPERTY_TYPE.BOOLEAN], + ['selected', PROPERTY_TYPE.BOOLEAN], + + // SVG 属性 + // xlink namespace 的 SVG 属性 + ['xlinkActuate', , 'xlink:actuate', 'http://www.w3.org/1999/xlink'], + ['xlinkArcrole', , 'xlink:arcrole', 'http://www.w3.org/1999/xlink'], + ['xlinkRole', , 'xlink:role', 'http://www.w3.org/1999/xlink'], + ['xlinkShow', , 'xlink:show', 'http://www.w3.org/1999/xlink'], + ['xlinkTitle', , 'xlink:title', 'http://www.w3.org/1999/xlink'], + ['xlinkType', , 'xlink:type', 'http://www.w3.org/1999/xlink'], + // xml namespace 的 SVG 属性 + ['xmlBase', , 'xml:base', 'http://www.w3.org/XML/1998/namespace'], + ['xmlLang', , 'xml:lang', 'http://www.w3.org/XML/1998/namespace'], + ['xmlSpace', , 'xml:space', 'http://www.w3.org/XML/1998/namespace'], + // HTML and SVG 中都有的属性,大小写敏感 + ['tabIndex', , 'tabindex'], + ['crossOrigin', , 'crossorigin'], + // 接受 URL 的属性 + ['xlinkHref', , 'xlink:href', 'http://www.w3.org/1999/xlink'], + ['formAction', , 'formaction'], +]; + +const propsDetailData = {}; + +propertiesData.forEach(record => { + const propName = record[0]; + let [type, attrName, attrNS] = record.slice(1) as PropData[]; + + if (type === undefined) { + type = PROPERTY_TYPE.STRING; + } + + if (!attrName) { + attrName = propName; + } + + if (!attrNS) { + attrNS = null; + } + + propsDetailData[propName!] = { + propName, + type, + attrName, + attrNS, + }; +}); + +export function getPropDetails(name: string): PropDetails | null { + return propsDetailData[name] || null; +} diff --git a/packages/inula-reactive/src/dom/validators/ValidateProps.ts b/packages/inula-reactive/src/dom/validators/ValidateProps.ts new file mode 100644 index 00000000..2a6b27e1 --- /dev/null +++ b/packages/inula-reactive/src/dom/validators/ValidateProps.ts @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { getPropDetails, PROPERTY_TYPE, PropDetails } from './PropertiesData'; + +const INVALID_EVENT_NAME_REGEX = /^on[^A-Z]/; + +// 是内置元素 +export function isNativeElement(tagName: string, props: Record) { + return !tagName.includes('-') && props.is === undefined; +} + +function isInvalidBoolean(attributeName: string, value: any, propDetails: PropDetails): boolean { + if (propDetails.type === PROPERTY_TYPE.SPECIAL) { + return false; + } + + // 布尔值校验 + if (typeof value === 'boolean') { + const isBooleanType = propDetails.type === PROPERTY_TYPE.BOOLEAN_STR || propDetails.type === PROPERTY_TYPE.BOOLEAN; + + if (isBooleanType || (attributeName.startsWith('data-') && attributeName.startsWith('aria-'))) { + return false; + } + + // 否则有问题 + return true; + } + + return false; +} + +// 是事件属性 +export function isEventProp(propName) { + return propName.substr(0, 2) === 'on'; +} + +function isValidProp(tagName, name, value) { + // 校验事件名称 + if (isEventProp(name)) { + // 事件名称不满足小驼峰 + if (INVALID_EVENT_NAME_REGEX.test(name)) { + console.error('Invalid event property `%s`, events use the camelCase name.', name); + } + return true; + } + + const propDetails = getPropDetails(name); + + // 当已知属性为错误类型时发出警告 + if (propDetails !== null && isInvalidBoolean(name, value, propDetails)) { + return false; + } + + return true; +} + +export function isInvalidValue( + name: string, + value: any, + propDetails: PropDetails | null, + isNativeTag: boolean +): boolean { + if (value === null || value === undefined) { + return true; + } + + if (!isNativeTag) { + return false; + } + + if (propDetails !== null && isInvalidBoolean(name, value, propDetails)) { + return true; + } + + if (propDetails !== null && propDetails.type === PROPERTY_TYPE.BOOLEAN) { + return !value; + } + + return false; +} + +// dev模式下校验属性是否合法 +export function validateProps(type, props) { + if (!props) { + return; + } + + // 非内置的元素 + if (!isNativeElement(type, props)) { + return; + } + + // style属性必须是对象 + if (props.style !== null && props.style !== undefined && typeof props.style !== 'object') { + throw new Error('style should be a object.'); + } + + if (isDev) { + // 校验属性 + const invalidProps = Object.keys(props).filter(key => !isValidProp(type, key, props[key])); + + const propString = invalidProps.map(prop => '`' + prop + '`').join(', '); + + if (invalidProps.length >= 1) { + console.error('Invalid value for prop %s on <%s> tag.', propString, type); + } + } +} diff --git a/packages/inula-reactive/src/dom/valueHandler/InputValueHandler.ts b/packages/inula-reactive/src/dom/valueHandler/InputValueHandler.ts new file mode 100644 index 00000000..cc6d6f8e --- /dev/null +++ b/packages/inula-reactive/src/dom/valueHandler/InputValueHandler.ts @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { updateCommonProp } from '../DOMPropertiesHandler/UpdateCommonProp'; +import { Props } from '../utils/Interface'; +import { getValue } from '../../reactive/Utils'; +import { handleReactiveProp } from '../../reactive/RContextCreator'; + +function getInitValue(dom: HTMLInputElement, props: Props) { + const { value, defaultValue, checked, defaultChecked } = props; + + const defaultValueStr = defaultValue !== null && defaultValue !== undefined ? defaultValue : ''; + const initValue = value !== null && value !== undefined ? value : defaultValueStr; + const initChecked = checked !== null && checked !== undefined ? checked : defaultChecked; + + return { initValue, initChecked }; +} + +export function getInputPropsWithoutValue(dom: HTMLInputElement, props: Props) { + // checked属于必填属性,无法置 + let { checked } = props; + if (checked === undefined) { + checked = getInitValue(dom, props).initChecked; + } + + return { + ...props, + value: undefined, + defaultValue: undefined, + defaultChecked: undefined, + checked, + }; +} + +export function updateInputValue(dom: HTMLInputElement, props: Props) { + const { value, checked } = props; + + const val = getValue(value); + + if (val !== undefined) { + // 处理 dom.value 逻辑 + if (dom.value !== String(val)) { + dom.value = String(val); + } + } else if (checked !== undefined) { + updateCommonProp(dom, 'checked', checked, true); + } +} + +// 设置input的初始值 +export function setInitInputValue(dom: HTMLInputElement, props: Props) { + const { value, defaultValue } = props; + const { initValue, initChecked } = getInitValue(dom, props); + + if (value !== undefined || defaultValue !== undefined) { + // value 的使用优先级 value 属性 > defaultValue 属性 > 空字符串 + const initValueStr = getValue(initValue); + + handleReactiveProp(dom, 'value', value); + + dom.value = initValueStr; + + dom.defaultValue = initValueStr; + } + + // checked 的使用优先级 checked 属性 > defaultChecked 属性 > false + dom.defaultChecked = Boolean(initChecked); +} diff --git a/packages/inula-reactive/src/dom/valueHandler/OptionValueHandler.ts b/packages/inula-reactive/src/dom/valueHandler/OptionValueHandler.ts new file mode 100644 index 00000000..13532ff5 --- /dev/null +++ b/packages/inula-reactive/src/dom/valueHandler/OptionValueHandler.ts @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { Children } from '../../external/ChildrenUtil'; +import { Props } from '../utils/Interface'; + +// 把 const a = 'a'; 转成 giraffe +function concatChildren(children) { + let content = ''; + Children.forEach(children, function (child) { + content += child; + }); + + return content; +} + +export function getOptionPropsWithoutValue(dom: Element, props: Props) { + const content = concatChildren(props.children); + + return { + ...props, + children: content || undefined, // 覆盖children + }; +} diff --git a/packages/inula-reactive/src/dom/valueHandler/SelectValueHandler.ts b/packages/inula-reactive/src/dom/valueHandler/SelectValueHandler.ts new file mode 100644 index 00000000..8308d44f --- /dev/null +++ b/packages/inula-reactive/src/dom/valueHandler/SelectValueHandler.ts @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { InulaSelect, Props } from '../utils/Interface'; + +function updateMultipleValue(options, newValues) { + const newValueSet = new Set(); + + newValues.forEach(val => { + newValueSet.add(String(val)); + }); + + // options 非数组 + for (let i = 0; i < options.length; i++) { + const option = options[i]; + const newValue = newValueSet.has(option.value); + + if (option.selected !== newValue) { + option.selected = newValue; + } + } +} + +// 单选时传入的选项参数必须是可以转为字符串的类型 +function updateSingleValue(options, newValue) { + for (let i = 0; i < options.length; i++) { + const option = options[i]; + + if (option.value === String(newValue)) { + option.selected = true; + break; + } + } +} + +// 更新