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/no-vnode/no-vnode-base.test.js b/packages/inula-reactive/scripts/__tests__/ReactivityTest/no-vnode/no-vnode-base.test.js new file mode 100644 index 00000000..947eff0f --- /dev/null +++ b/packages/inula-reactive/scripts/__tests__/ReactivityTest/no-vnode/no-vnode-base.test.js @@ -0,0 +1,558 @@ +import Inula, { computed, createRef, reactive, watchReactive } from '../../../../src/index'; +import { template as _$template, insert as _$insert, setAttribute as _$setAttribute } from '../../../../src/no-vnode/dom'; +import { createComponent as _$createComponent, render } from '../../../../src/no-vnode/core'; +import { delegateEvents as _$delegateEvents, addEventListener as _$addEventListener } from '../../../../src/no-vnode/event'; + +import { Show } from '../../../../src/no-vnode/components/Show'; +import { For } from '../../../../src/no-vnode/components/For'; + +describe('测试 no-vnode', () => { + it('简单的使用signal', () => { + /** + * 源码: + * const CountingComponent = () => { + * const [count, setCount] = useSignal(0); + * + * return
    Count value is {count()}.
    ; + * }; + * + * render(() => , container); + */ + + let g_count; + + // 编译后: + const _tmpl$ = /*#__PURE__*/ _$template(`
    Count value is .`); + const CountingComponent = () => { + const count = reactive(0); + g_count = count; + + return (() => { + const _el$ = _tmpl$(), + _el$2 = _el$.firstChild, + _el$4 = _el$2.nextSibling, + _el$3 = _el$4.nextSibling; + _$insert(_el$, count, _el$4); + return _el$; + })(); + }; + render(() => _$createComponent(CountingComponent, {}), container); + + _$delegateEvents(['click']); + + expect(container.querySelector('#count').innerHTML).toEqual('Count value is 0.'); + + g_count.set(c => c + 1); + + expect(container.querySelector('#count').innerHTML).toEqual('Count value is 1.'); + }); + + it('return数组,click事件', () => { + /** + * 源码: + * const CountingComponent = () => { + * const [count, setCount] = createSignal(0); + * const add = () => { + * setCount((c) => c + 1); + * } + * return <> + *
    Count value is {count()}.
    + *
    + * ; + * }; + */ + + // 编译后: + const _tmpl$ = /*#__PURE__*/ _$template(`
    Count value is .`), + _tmpl$2 = /*#__PURE__*/ _$template(`
    + *
    ; + * }; + * + * render(() => , document.getElementById("app")); + */ + + // 编译后: + const _tmpl$ = /*#__PURE__*/ _$template(`
    Count value is .`), + _tmpl$2 = /*#__PURE__*/ _$template(`
    + *
    ; + * }; + * + * render(() => , document.getElementById("app")); + */ + + // 编译后: + const _tmpl$ = /*#__PURE__*/ _$template(`
    Count value is .`), + _tmpl$2 = /*#__PURE__*/ _$template(`
    + *
    + *
    ; + * }; + * + * render(() => , document.getElementById("app")); + */ + + // 编译后: + const _tmpl$ = /*#__PURE__*/_$template(`
    Count value is .`), + _tmpl$2 = /*#__PURE__*/_$template(`
    + *
    + * ); + * + * const Main = () => { + * const [state, setState] = createStore({data: [{id: 1, label: '111', selected: false}, {id: 2, label: '222', selected: false}], num: 2}); + * + * function run() { + * setState('data', buildData(5)); + * } + * + * return ( + *
    + *
    + *
    + *

    Horizon-reactive-novnode

    + *
    + *
    + *
    + *
    + *
    + *
    + * + * + *
    + *
    + * ); + * }; + * + * render(() =>
    , document.getElementById("app")); + */ + + // 编译后: + const _tmpl$ = /*#__PURE__*/_$template(``), + _tmpl$2 = /*#__PURE__*/_$template(`