Match-id-1e4919b0867dd1554cb4c1d2558d8066882e71f6

This commit is contained in:
* 2022-08-03 10:47:16 +08:00 committed by *
commit bdd2ad3cce
117 changed files with 1715 additions and 4530 deletions

37
.cloudbuild/build.yml Normal file
View File

@ -0,0 +1,37 @@
version: '2.0'
notifications:
notifier:
espace:
'on': false
email:
'on': false
buildspace:
log_collect:
- config/CI/build/logs
fixed: true
env:
resource:
type: docker
image: kweecr04.his.huawei.com:80/ecr-build-arm-gzkunpeng/euleros_v2r7spc522_x64_opmt_cs5.0_sz:v5.0
class: 4U8G
mode: toolbox
cache:
- type: workspace
steps:
PRE_BUILD:
- checkout:
path: horizon-core
- gitlab:
url: https://szv-y.codehub.huawei.com/CloudSOP/CloudSOP-CI.git
branch: $branch
path: CI
BUILD:
- build_execute:
command: |
npm install yarn -g
yarn config set strict-ssl false
cd horizon-core
yarn
yarn run test
yarn run build
node .cloudbuild/release.js

60
.cloudbuild/release.js Normal file
View File

@ -0,0 +1,60 @@
/*
* Copyright (c) Huawei Technologies Co., Ltd. 2022-2022. All rights reserved.
*/
const fs = require('fs');
const path = require('path');
const childProcess = require('child_process');
const version = process.env.releaseVersion;
const DIST_PATH = path.resolve(__dirname, '../build/horizon');
const NPMRC = `registry=https://cmc.centralrepo.rnd.huawei.com/npm
@cloudsop:registry=https://cmc.centralrepo.rnd.huawei.com/artifactory/product_npm
_auth = Y2xvdWRzb3BhcnRpZmFjdG9yeTpDbG91ZHNvcDY2NiEhIQ
always-auth = true
email = cloudsop@huawei.com
`;
if (!version) {
process.exit();
}
if (!/\d+\.\d+\.\d+/.test(version)) {
console.log('请输入正确版本号');
process.exit();
}
const exec = (cmd, cwd) => {
return new Promise((resolve, reject) => {
childProcess.exec(
cmd,
{
cwd,
},
function (error, stdout, stderr) {
if (error) {
error && console.log(`Error: ${error}`);
reject(error);
} else {
stdout && console.log(`STDOUT: ${stdout}`);
resolve(stdout);
}
}
);
});
};
const main = async () => {
try {
console.log(`==== Horizon Upgrade ${version} ====`);
await exec(`npm version ${version}`, DIST_PATH);
fs.writeFileSync(path.resolve(DIST_PATH, '.npmrc'), NPMRC);
console.log('==== Publish new version====');
await exec('npm publish', DIST_PATH);
process.exit();
} catch (err) {
console.error(err);
process.exit(1);
}
};
main();

37
.cloudbuild/test.yml Normal file
View File

@ -0,0 +1,37 @@
version: '2.0'
notifications:
notifier:
espace:
'on': false
email:
'on': false
buildspace:
log_collect:
- config/CI/build/logs
fixed: true
env:
resource:
type: docker
image: kweecr04.his.huawei.com:80/ecr-build-arm-gzkunpeng/euleros_v2r7spc522_x64_opmt_cs5.0_sz:v5.0
class: 4U8G
mode: toolbox
cache:
- type: workspace
steps:
PRE_BUILD:
- checkout:
path: horizon-core
- gitlab:
url: https://szv-open.codehub.huawei.com/innersource/shanhai/wutong/react/horizon-test.git
branch: one_tree_dev
path: horizon-test
BUILD:
- build_execute:
command: |
npm install yarn -g
yarn config set strict-ssl false
cd horizon-core
yarn
cd ../horizon-test
yarn
yarn run test

View File

@ -1,3 +1,12 @@
## 0.0.12 (2022-07-25)
- 修复IE兼容性问题IE环境下Event只读导致合成事件逻辑报错
## 0.0.11 (2022-07-21)
### Bug Fixes
- **horizonX**: 修复IE兼容性问题空catch导致崩溃
- 增加`Object.assign`的babel polyfill
-
## 0.0.10 (2022-07-14) ## 0.0.10 (2022-07-14)
- **core**: #24 修复lazy包裹memo时卸载错误 - **core**: #24 修复lazy包裹memo时卸载错误
- **core**: #21 修复异步更新时路径错误 - **core**: #21 修复异步更新时路径错误

View File

@ -11,6 +11,7 @@ module.exports = {
{ loose: true, useBuiltIns: true }, { loose: true, useBuiltIns: true },
], ],
['@babel/plugin-transform-template-literals', { loose: true }], ['@babel/plugin-transform-template-literals', { loose: true }],
'@babel/plugin-transform-object-assign',
'@babel/plugin-transform-literals', '@babel/plugin-transform-literals',
'@babel/plugin-transform-arrow-functions', '@babel/plugin-transform-arrow-functions',
'@babel/plugin-transform-block-scoped-functions', '@babel/plugin-transform-block-scoped-functions',

View File

@ -1,27 +0,0 @@
{
"presets": [
"@babel/react",
"@babel/typescript",
[
"@babel/env",
{
"modules": false
}
]
],
"plugins": [
[
"@babel/plugin-proposal-class-properties",
{
"loose": true
}
],
[
"@babel/plugin-transform-react-jsx",
{
"pragma": "Horizon.createElement",
"pragmaFrag": "Horizon.Fragment"
}
]
]
}

View File

@ -1,3 +0,0 @@
Horizon X antd demo:
1. run `npm run build:watch` in root's `package.json`
2. run `npm start` to run Horizon X antd

View File

@ -1,38 +0,0 @@
import Horizon from 'horizon';
import { AppstoreOutlined } from '@ant-design/icons';
import { Menu } from 'antd';
function getItem(label, key, icon, children, type) {
return {
key,
icon,
children,
label,
type,
};
}
const items = [
getItem('sub2', 'sub2', <AppstoreOutlined />, [getItem('sub3', 'sub3', null, [getItem('sub4', 'sub4')])]),
];
const App = () => {
const onClick = e => {
console.log('click ', e);
};
return (
<Menu
onClick={onClick}
style={{
width: 256,
}}
defaultSelectedKeys={['sub2']}
defaultOpenKeys={['sub2', 'sub3']}
mode="inline"
items={items}
/>
);
};
export default App;

View File

@ -1,73 +0,0 @@
import Horizon, { useState } from 'horizon';
import {
AppstoreOutlined,
ContainerOutlined,
MenuFoldOutlined,
PieChartOutlined,
DesktopOutlined,
MailOutlined,
MenuUnfoldOutlined,
} from '@ant-design/icons';
import { Button, Menu } from 'antd';
function getItem(label, key, icon, children, type) {
return {
key,
icon,
children,
label,
type,
};
}
const items = [
getItem('选项1', '1', <PieChartOutlined />),
getItem('选项2', '2', <DesktopOutlined />),
getItem('选项3', '3', <ContainerOutlined />),
getItem('分组1', 'sub1', <MailOutlined />, [
getItem('选项5', '5'),
getItem('选项6', '6'),
getItem('选项7', '7'),
getItem('选项8', '8'),
]),
getItem('分组2', 'sub2', <AppstoreOutlined />, [
getItem('选项9', '9'),
getItem('选项10', '10'),
getItem('分组2-1', 'sub3', null, [getItem('选项11', '11'), getItem('选项12', '12')]),
]),
];
const App = () => {
const [collapsed, setCollapsed] = useState(false);
return (
<div
style={{
width: 256,
marginLeft: 32,
}}
>
<Button
type="primary"
onClick={() => {
setCollapsed(!collapsed);
}}
style={{
marginBottom: 16,
}}
>
{collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
</Button>
<Menu
mode="inline"
theme="dark"
defaultSelectedKeys={['2']}
defaultOpenKeys={['sub2']}
inlineCollapsed={collapsed}
items={items}
/>
</div>
);
};
export default App;

View File

@ -1,97 +0,0 @@
import Horizon from 'horizon';
import { Table } from 'antd';
const columns = [
{
title: 'Full Name',
width: 100,
dataIndex: 'name',
key: 'name',
fixed: 'left',
},
{
title: 'Lang',
width: 100,
dataIndex: 'lang',
key: 'age',
fixed: 'left',
},
{
title: 'COL1',
dataIndex: 'description',
key: '1',
width: 220,
},
{
title: 'COL2',
dataIndex: 'description',
key: '2',
width: 220,
},
{
title: 'COL3',
dataIndex: 'description',
key: '3',
width: 220,
},
{
title: 'COL4',
dataIndex: 'description',
key: '4',
width: 220,
},
{
title: 'COL5',
dataIndex: 'description',
key: '5',
width: 220,
},
{
title: 'COL6',
dataIndex: 'description',
key: '6',
width: 220,
},
{
title: 'COL7',
dataIndex: 'description',
key: '7',
width: 220,
},
{
title: 'COL8',
dataIndex: 'description',
key: '8',
},
{
title: 'Action',
key: 'operation',
fixed: 'right',
width: 100,
render: () => <a>action</a>,
},
];
const data = [];
for (let i = 0; i < 100; i++) {
data.push({
key: i,
name: `Horizon ${i}`,
lang: 'js',
description: `Javascript Framework no. ${i}`,
});
}
const App = () => (
<div style={{ width: '1200px' }}>
<Table
columns={columns}
dataSource={data}
scroll={{
x: 2200,
y: 300,
}}
/>
</div>
);
export default App;

View File

@ -1,29 +0,0 @@
import Horizon from 'horizon';
import 'antd/dist/antd.css';
import Table from './components/Table';
import Menu from './components/Menu';
import Menu2 from './components/Menu2';
import { Tabs } from 'antd';
const { TabPane } = Tabs;
const onChange = key => {
console.log(key);
};
const App = () => (
<div style={{ padding: '12px' }}>
<h1>Horizon antd</h1>
<Tabs defaultActiveKey="Menu" onChange={onChange}>
<TabPane tab="Table" key="Table">
<Table />
</TabPane>
<TabPane tab="Menu" key="Menu">
<div style={{ display: 'flex' }}>
<Menu />
<Menu2 />
</div>
</TabPane>
</Tabs>
</div>
);
Horizon.render(<App key={1} />, document.getElementById('app'));

View File

@ -1,34 +0,0 @@
{
"name": "horizon-antd",
"version": "1.0.0",
"description": "",
"scripts": {
"start": "webpack-dev-server --config webpack.dev.js --hot --mode development --open"
},
"license": "MIT",
"dependencies": {
"@ant-design/icons": "^4.7.0",
"@babel/polyfill": "^7.10.4",
"antd": "^4.21.3",
"css-loader": "^5.2.2",
"style-loader": "^2.0.0"
},
"devDependencies": {
"@babel/core": "^7.11.1",
"@babel/plugin-proposal-class-properties": "^7.10.4",
"@babel/plugin-proposal-object-rest-spread": "^7.11.0",
"@babel/plugin-syntax-jsx": "^7.10.4",
"@babel/preset-env": "^7.11.0",
"@babel/preset-react": "^7.10.4",
"@babel/preset-typescript": "^7.10.4",
"@hot-loader/react-dom": "16.9.0",
"babel-loader": "^8.1.0",
"html-webpack-plugin": "^3.2.0",
"html-webpack-template": "^6.2.0",
"react-hot-loader": "^4.12.20",
"webpack": "4.42.0",
"webpack-cli": "3.3.11",
"webpack-dev-server": "^3.10.3",
"webpack-watch-files-plugin": "^1.2.1"
}
}

View File

@ -1,54 +0,0 @@
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const horizon = path.resolve(__dirname, '../../build/horizon');
const config = () => {
return {
entry: ['./index.jsx'],
output: {
path: path.resolve(__dirname, 'temp'),
filename: '[name].[hash].js',
},
devtool: 'source-map',
module: {
rules: [
{
test: /\.ts(x)?|js|jsx$/,
exclude: /node_modules/,
loader: 'babel-loader',
},
{
test: /\.css$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 1,
},
},
],
exclude: /\.module\.css$/,
},
],
},
resolve: {
extensions: ['.tsx', '.ts', '.js', '.jsx', 'json'],
alias: {
horizon: horizon,
react: horizon,
'react-dom': horizon,
},
},
plugins: [
new HtmlWebpackPlugin({
template: require('html-webpack-template'),
title: 'Horizon Antd',
inject: false,
appMountId: 'app',
filename: 'index.html',
}),
],
};
};
module.exports = config;

View File

@ -11,7 +11,8 @@ module.exports = {
testEnvironment: 'jest-environment-jsdom-sixteen', testEnvironment: 'jest-environment-jsdom-sixteen',
testMatch: [ testMatch: [
'<rootDir>/scripts/__tests__/**/*.test.js' '<rootDir>/scripts/__tests__/**/*.test.js',
'<rootDir>/scripts/__tests__/**/*.test.tsx'
], ],
timers: 'fake', timers: 'fake',

View File

@ -1,15 +0,0 @@
module.exports = api => {
const isTest = api.env('test');
console.log('isTest', isTest);
return {
presets: [
'@babel/preset-env',
'@babel/preset-typescript',
['@babel/preset-react', {
runtime: 'classic',
'pragma': 'Horizon.createElement',
'pragmaFrag': 'Horizon.Fragment',
}]],
plugins: ['@babel/plugin-proposal-class-properties'],
};
};

View File

@ -1,35 +0,0 @@
{
"name": "extension",
"version": "1.0.0",
"description": "",
"main": "",
"scripts": {
"build": "webpack --config ./webpack.config.js",
"watch": "webpack --config ./webpack.config.js --watch",
"build-dev": "webpack --config ./webpack.dev.js",
"start": "npm run build && webpack serve --config ./webpack.dev.js ",
"test": "jest"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/core": "7.12.3",
"@babel/plugin-proposal-class-properties": "7.16.7",
"@babel/preset-env": "7.12.1",
"@babel/preset-react": "7.12.1",
"@babel/preset-typescript": "7.16.7",
"@types/jest": "27.4.1",
"babel-loader": "8.1.0",
"css-loader": "6.7.1",
"html-webpack-plugin": "5.5.0",
"jest": "27.5.1",
"less": "4.1.2",
"less-loader": "10.2.0",
"style-loader": "3.3.1",
"ts-jest": "27.1.4",
"webpack": "5.70.0",
"webpack-cli": "4.9.2",
"webpack-dev-server": "^4.7.4"
}
}

View File

@ -1,165 +0,0 @@
## 为什么要做 devTool 插件
让Horizon开发者获得更好的开发体验获取准确的组件树结构、状态信息和真实dom对应关系。
## 上下文关系
devTool功能的实现依赖浏览器 extension 开放的能力,用于绘制展示组件信息和获取真实 dom 元素。同时也需要 Horizon 提供相关接口获取组件树信息和提供调试能力。
## 目标
1. 查看组件树结构并支持过滤
2. 查看组件与真实dom的关系
3. 查看组件props, state, hooks 等信息
4. 调试单个组件及其子组件
5. 支持状态管理解决方案调试
## 和 react devTool 能力对比
||react | Horizon|
|-|-|-|
|查看组件树|Y |Y |
|查看真实DOM|Y|Y|
|查看组件信息|Y|Y|
|调试能力|Y| Y |
|性能调试|Y|N|
|解析Hook名|Y|N|
|状态管理解决方案调试|N|Y|
## 架构草图
```plantuml
@startuml
package "Horizon" {
[U I]
[Helper]
}
package "Script Content" {
[GlobalHook]
[MessageHandler]
[Parser]
}
package "Browser" {
[Background]
[Panel]
}
[GlobalHook] <-- [U I]
[GlobalHook] --> [MessageHandler]
[Helper] <-- [MessageHandler]
[Helper] --> [U I]
[MessageHandler] <--> [Background]
[Background] <--> [Panel]
[Parser] --> [MessageHandler]
@enduml
```
#### 说明
Helper: 提供接口给插件操控组件以及提供工具方法。
Parser: 负责将组件树结构和组件信息解析成特定的数据结构供Panel展示。
## 文件清单说明:
devtools_page: devtool主页面
default_popup: 拓展图标点击时弹窗页面
content_scripts: 内容脚本,在项目中负责在页面初始化时调用注入全局变量代码和消息传递
## 打开 panel 页面调试面板的方式
1. Open the developer tools.
1. Undock the developer tools if not already done (via the button in the bottom-left corner).
1. Press Ctrl + Shift + J to open the developer tools of the developer tools.
Optional: Feel free to dock the developer tools again if you had undocked it at step 2.
1. Switch from "<top frame>" to devtoolsBackground.html (or whatever name you have chosen for your devtools). (example)
1. Now you can use the Console tab to play with the chrome.devtools API.
## 全局变量注入
通过content_scripts在document初始化时给页面添加script脚本在新添加的脚本中给window注入全局变量
## 通信方式:
```mermaid
sequenceDiagram
participant web_page
participant script_content
participant background
participant panel
Note over web_page: window.postMessage
web_page ->> script_content : data
Note over script_content: window.addEventListener
Note over script_content: chrome.runtime.sendMessage
script_content ->> background : data
Note over background: chrome.runtime.onMessage
Note over background: port.postMessage
background ->> panel : data
Note over panel: connection.onMessage.addListener
Note over panel: connection.postMessage
panel ->> background : data
Note over background: port.onMessage.addListener
Note over background: chrome.tabs.sendMessage
background ->> script_content : data
Note over script_content: chrome.runtime.onMessage
Note over script_content: window.postMessage
script_content ->> web_page : data
Note over web_page: window.addEventListener
```
## 传输数据结构
**<font color=#8B0000>限制chrome.runtime.sendMessage只能传递 JSON-serializable 数据</font>**
```ts
type passData = {
type: 'HORIZON_DEV_TOOLS',
payload: {
type: string,
data: any,
},
from: string,
}
```
## horizon和devTools的主要交互
- App初始渲染
- App更新
- App销毁
- 整个页面刷新
- devTools触发组件属性更新
## 对组件的操作
我们希望插件和Horizon能够尽量解耦所以Horizon提供了Helper注入给插件提供相关方法操作组件。
## 触发组件更新方式
- 类组件的state调用实例的 setState 函数触发更新
- 类组件的props浅复制props后更新props值并调用 forceUpdate 触发更新
- 函数组件的props新增了devProps属性在特定时刻重新给props赋值触发更新
- 函数组件的state调用 useState 函数触发更新
## VNode的清理
全局 hook 中保存了root VNode在解析 VNode 树的时候也会保存 VNode 的引用在清理VNode的时候这些 VNode 的引用也需要删除。
## 数据压缩
渲染组件树需要知道组件名和层次信息如果把整个vNode树传递过来传递对象太大最好将数据进行压缩然后传递。
- 相同的组件名可以进行压缩
- 每个vNode有唯一的 path 属性,可以作为标识使用
- 通过解析 path 值可以分析出组件树的结构
## 组件props/state/hook等数据的传输和解析
将数据格式进行转换后进行传递。对于 props 和 类组件的 state他们都是对象可以将对象进行解析然后以 k-v 的形式,树的结构显示。函数组件的 Hooks 是以数组的形式存储在 vNode 的属性中的,每个 hook 的唯一标识符是 hIndex 属性值,在对象展示的时候不能展示该属性值,需要根据 hook 类型展示一个 state/ref/effect 等值。hook 中存储的值也可能不是对象,只是一个简单的字符串或者 dom 元素,他们的解析和 props/state 的解析同样存在差异,需要单独处理。
## 滚动动态渲染 Tree
考虑到组件树可能很大,所以并不适合一次性全部渲染出来,可以通过滚动渲染的方式减少页面 dom 的数量。我们可以把树看成有不同缩进长度的列表动态渲染滚动列表的实现可以参考谷歌的这篇文章https://developers.google.com/web/updates/2016/07/infinite-scroller 这样,我们需要的组件树数据可以由树结构转变为数组,可以减少动态渲染时对树结构进行解析时的计算工作。
## 虚拟列表针对 UI 框架的优化
列表中增减不同 key 项意味着 dom 增删,我们需要让框架尽可能减少 dom 操作。
- 不管渲染列表项怎么变化,应该始终以 index 作为 key这样只会更新 dom 的属性,不会有 dom 增删操作。
- 如果在滚动过程中,一个 item 没有被移出渲染列表,它在列表中的 key 值不应该发生变化,由于 item 本身的数据没有变化,所以渲染的 children 也不会发生变化。结合上条的结论,它的属性值也不会变化,所以该 item 对应的 dom 都不会更新。
## 开发者页面打开场景
- 先有页面然后打开开发者工具工具建立连接发送通知页面hook收到后发送VNode树信息给工具页面
- 已经打开开发者工具然后打开页面业务页面渲染完毕发送VNode树信息给工具页面
## 开发者工具页面响应组件树变更
组件树变更会带来新旧两个组件树信息数组新旧数组存在数据一致而引用不一致的情况而VTree和VList组件中相关信息的计算依赖引用而非数据本身在收到新的组件树信息后需要对数据本身进行判断将新数组中的相同数据使用旧对象代替。
## 测试框架
jest测试框架不提供浏览器插件的相关 api我们在封装好相关 api 后需要模拟这些 api 的行为从而展开测试工作。

View File

@ -1,62 +0,0 @@
import { checkMessage, packagePayload, changeSource } from '../utils/transferTool';
import { RequestAllVNodeTreeInfos, InitDevToolPageConnection, DevToolBackground } from '../utils/constants';
import { DevToolPanel, DevToolContentScript } from '../utils/constants';
// 多个页面、tab页共享一个 background需要建立连接池给每个tab建立连接
const connections = {};
// panel 代码中调用 let backgroundPageConnection = chrome.runtime.connect({...}) 会触发回调函数
chrome.runtime.onConnect.addListener(function (port) {
function extensionListener(message) {
const isHorizonMessage = checkMessage(message, DevToolPanel);
if (isHorizonMessage) {
const { payload } = message;
// tabId 值指当前浏览器分配给 web_page 的 id 值。是panel页面查询得到指定向该页面发送消息
const { type, data, tabId } = payload;
let passMessage;
if (type === InitDevToolPageConnection) {
// 记录 panel 所在 tab 页的tabId如果已经记录了覆盖原有port因为原有port可能关闭了
// 可能这次是 panel 发起的重新建立请求
connections[tabId] = port;
passMessage = packagePayload({ type: RequestAllVNodeTreeInfos }, DevToolBackground);
} else {
passMessage = packagePayload({type, data}, DevToolBackground);
}
chrome.tabs.sendMessage(tabId, passMessage);
}
}
// Listen to messages sent from the DevTools page
port.onMessage.addListener(extensionListener);
port.onDisconnect.addListener(function (port) {
port.onMessage.removeListener(extensionListener);
const tabs = Object.keys(connections);
for (let i = 0, len = tabs.length; i < len; i++) {
if (connections[tabs[i]] == port) {
delete connections[tabs[i]];
break;
}
}
});
});
// 监听来自 content script 的消息,并将消息发送给对应的 devTools page也就是 panel
chrome.runtime.onMessage.addListener(function (message, sender, sendResponse) {
// Messages from content scripts should have sender.tab set
if (sender.tab) {
const tabId = sender.tab.id;
// 和 InitDevToolPageConnection 时得到的 tabId 值一致时,向指定的 panel 页面 port 发送消息
if (tabId in connections && checkMessage(message, DevToolContentScript)) {
changeSource(message, DevToolBackground);
connections[tabId].postMessage(message);
} else {
// TODO: 如果查询失败,发送 chrome message请求 panel 主动建立连接
console.log('Tab not found in connection list.');
}
} else {
console.log('sender.tab not defined.');
}
// 需要返回消息告知完成通知,否则会出现报错 message port closed before a response was received
sendResponse({status: 'ok'});
});

View File

@ -1,175 +0,0 @@
import styles from './ComponentsInfo.less';
import Eye from '../svgs/Eye';
import Debug from '../svgs/Debug';
import Triangle from '../svgs/Triangle';
import { useState, useEffect } from 'horizon';
import { IData } from './VTree';
import { buildAttrModifyData, IAttr } from '../parser/parseAttr';
import { postMessageToBackground } from '../panelConnection';
import { InspectDom, LogComponentData, ModifyAttrs } from '../utils/constants';
type IComponentInfo = {
name: string;
attrs: {
parsedProps?: IAttr[],
parsedState?: IAttr[],
parsedHooks?: IAttr[],
};
parents: IData[];
id: number;
onClickParent: (item: IData) => void;
};
function collapseAllNodes(attrs: IAttr[]) {
return attrs.filter((item, index) => {
const nextItem = attrs[index + 1];
return nextItem ? nextItem.indentation - item.indentation > 0 : false;
});
}
function ComponentAttr({ attrsName, attrsType, attrs, id }: {
attrsName: string,
attrsType: string,
attrs: IAttr[],
id: number}) {
const [collapsedNode, setCollapsedNode] = useState(collapseAllNodes(attrs));
const [editableAttrs, setEditableAttrs] = useState(attrs);
useEffect(() => {
setCollapsedNode(collapseAllNodes(attrs));
setEditableAttrs(attrs);
}, [attrs]);
const handleCollapse = (item: IAttr) => {
const nodes = [...collapsedNode];
const i = nodes.indexOf(item);
if (i === -1) {
nodes.push(item);
} else {
nodes.splice(i, 1);
}
setCollapsedNode(nodes);
};
const showAttr = [];
let currentIndentation = null;
editableAttrs.forEach((item, index) => {
const indentation = item.indentation;
if (currentIndentation !== null) {
if (indentation > currentIndentation) {
return;
} else {
currentIndentation = null;
}
}
const nextItem = editableAttrs[index + 1];
const hasChild = nextItem ? nextItem.indentation - item.indentation > 0 : false;
const isCollapsed = collapsedNode.includes(item);
showAttr.push(
<div style={{ paddingLeft: item.indentation * 10 }} key={index} onClick={() => handleCollapse(item)}>
<span className={styles.attrArrow}>{hasChild && <Triangle director={isCollapsed ? 'right' : 'down'} />}</span>
<span className={styles.attrName}>{`${item.name}`}</span>
{' :'}
{item.type === 'string' || item.type === 'number' || item.type === 'undefined' || item.type === 'null'
? <input
value={String(item.value)}
className={styles.attrValue}
onChange={(event) => {
const nextAttrs = [...editableAttrs];
const nextItem = {...item};
nextItem.value = event.target.value;
nextAttrs[index] = nextItem;
setEditableAttrs(nextAttrs);
}}
onKeyUp={(event) => {
const value = (event.target as HTMLInputElement).value;
if (event.key === 'Enter') {
if(isDev) {
console.log('post attr change', value);
} else {
const data = buildAttrModifyData(attrsType,attrs, value,item, index, id);
postMessageToBackground(ModifyAttrs, data);
}
}
}}
/>
: (item.type === 'boolean'
? <input
type={'checkbox'}
checked={item.value}
className={styles.attrValue}
onChange={(event) => {
const nextAttrs = [...editableAttrs];
const nextItem = {...item};
nextItem.value = event.target.checked;
nextAttrs[index] = nextItem;
setEditableAttrs(nextAttrs);
if (!isDev) {
const data = buildAttrModifyData(attrsType,attrs, nextItem.value,item, index, id);
postMessageToBackground(ModifyAttrs, data);
}
}}/>
: <span className={styles.attrValue}>{item.value}</span>
)}
</div>
);
if (isCollapsed) {
currentIndentation = indentation;
}
});
return (
<div className={styles.attrContainer}>
<div className={styles.attrHead}>
<span className={styles.attrType}>{attrsName}</span>
</div>
<div className={styles.attrDetail}>
{showAttr}
</div>
</div>
);
}
export default function ComponentInfo({ name, attrs, parents, id, onClickParent }: IComponentInfo) {
return (
<div className={styles.infoContainer} >
<div className={styles.componentInfoHead}>
{name && <>
<span className={styles.name}>
{name}
</span>
<span className={styles.eye} title={'Inspect dom element'} onClick={() => {
postMessageToBackground(InspectDom, id);
}}>
<Eye />
</span>
<span className={styles.debug} title={'Log this component data'} onClick={() => {
postMessageToBackground(LogComponentData, id);
}}>
<Debug />
</span>
</>}
</div>
<div className={styles.componentInfoMain}>
{Object.keys(attrs).map(attrsType => {
const parsedAttrs = attrs[attrsType];
if (parsedAttrs && parsedAttrs.length !== 0) {
const attrsName = attrsType.slice(6); // parsedState => State
return <ComponentAttr attrsName={attrsName} attrs={parsedAttrs} id={id} attrsType={attrsType}/>;
}
return null;
})}
<div className={styles.parentsInfo}>
{name && <div>
parents: {
parents.map(item => (<button
className={styles.parent}
onClick={() => (onClickParent(item))}>
{item.name}
</button>))
}
</div>}
</div>
</div>
</div>
);
}

View File

@ -1,95 +0,0 @@
@import 'assets.less';
.infoContainer {
display: flex;
flex-direction: column;
height: 100%;
.componentInfoHead {
flex: 0 0 @top-height;
display: flex;
align-items: center;
border-bottom: @divider-style;
.name {
flex: 1 1 0;
padding: 0 1rem 0 1rem;
}
.eye {
flex: 0 0 1rem;
padding-right: 1rem;
cursor: pointer;
}
.debug {
flex: 0 0 1rem;
padding-right: 1rem;
cursor: pointer;
}
}
.componentInfoMain {
overflow-y: auto;
>:last-child {
border-bottom: unset;
}
>div {
border-bottom: @divider-style;
}
.attrContainer {
flex: 0 0;
.attrHead {
display: flex;
flex-direction: row;
align-items: center;
padding: 0.5rem 0.5rem 0 0.5rem;
.attrType {
flex: 1 1 0;
}
.attrCopy {
flex: 0 0 1rem;
padding-right: 1rem;
}
}
.attrDetail {
padding-bottom: 0.5rem;
.attrArrow {
color: @arrow-color;
width: 12px;
display: inline-block;
}
.attrName {
color: @attr-name-color;
}
.attrValue {
margin: 0 0 0 4px;
}
}
}
}
.parentsInfo {
flex: 1 1 0;
.parent {
display: block;
cursor: pointer;
text-align: left;
color: @component-name-color;
width: 100%;
&:hover {
background-color: @select-color;
}
}
}
}

View File

@ -1,78 +0,0 @@
/**
*
* ResizeObserver IE
* dom
* window resize object
* window object dom dom dom
* dom window window
* dom
*
* <div id='test'>
* <object> --> div
* <html></html> --> resize
* </object>
* </div>
*
*/
function timeout(fn) {
return setTimeout(fn, 20);
}
function requestFrame(fn) {
const raf = requestAnimationFrame || timeout;
return raf(fn);
}
function cancelFrame(id) {
const cancel = cancelAnimationFrame || clearTimeout;
cancel(id);
}
// 在闲置帧触发回调事件,如果在本次触发前存在未处理回调事件,
// 需要取消未处理的回调事件
function resizeListener(event) {
const win = event.target;
if (win.__resizeRAF__) {
cancelFrame(win.__resizeRAF__);
}
win.__resizeRAF__ = requestFrame(function () {
const observeElement = win.__observeElement__;
observeElement.__resizeCallbacks__.forEach(function (fn) {
fn.call(observeElement, observeElement, event);
});
});
}
function loadObserver() {
// 将待观测元素传递给 object 标签的 window 对象,这样在触发 resize 事件时可以拿到待观测元素
this.contentDocument.defaultView.__observeElement__ = this.__observeElement__;
// 给 html 的 window 对象添加 resize 事件
this.contentDocument.defaultView.addEventListener('resize', resizeListener);
}
export function addResizeListener(element: any, fn: any) {
if (!element.__resizeCallbacks__) {
element.__resizeCallbacks__ = [fn];
element.style.position = 'relative';
const observer = document.createElement('object');
observer.setAttribute('style', 'display: block; position: absolute; top: 0; left: 0; height: 100%; width: 100%; overflow: hidden; pointer-events: none; z-index: -1;');
observer.data = 'about:blank';
observer.onload = loadObserver;
observer.type = 'text/html';
observer['__observeElement__'] = element;
element.__observer__ = observer;
element.appendChild(observer);
} else {
element.__resizeCallbacks__.push(fn);
}
}
export function removeResizeListener(element, fn) {
element.__resizeCallbacks__.splice(element.__resizeCallbacks__.indexOf(fn), 1);
if (!element.__resizeCallbacks__.length) {
element.__observer__.contentDocument.defaultView.removeEventListener('resize', resizeListener);
element.removeChild(element.__observer__);
element.__observer__ = null;
}
}

View File

@ -1,3 +0,0 @@
.search {
width: 100%;
}

View File

@ -1,21 +0,0 @@
import styles from './Search.less';
interface SearchProps {
onChange: (event: any) => void,
value: string,
}
export default function Search(props: SearchProps) {
const { onChange, value } = props;
const handleChange = (event) => {
onChange(event.target.value);
};
return (
<input
onChange={handleChange}
className={styles.search}
value={value}
placeholder={'Search (text or /regex/)'}
/>
);
}

View File

@ -1,33 +0,0 @@
import { useEffect, useState, useRef } from 'horizon';
import { addResizeListener, removeResizeListener } from './ResizeEvent';
export function SizeObserver(props) {
const { children, ...rest } = props;
const containerRef = useRef<HTMLDivElement>();
const [size, setSize] = useState<{width: number, height: number}>();
const notifyChild = (element) => {
setSize({
width: element.offsetWidth,
height: element.offsetHeight,
});
};
useEffect(() => {
const element = containerRef.current;
setSize({
width: element.offsetWidth,
height: element.offsetHeight,
});
addResizeListener(element, notifyChild);
return () => {
removeResizeListener(element, notifyChild);
};
}, []);
const myChild = size ? children(size.width, size.height) : null;
return (
<div ref={containerRef} {...rest}>
{myChild}
</div>
);
}

View File

@ -1,62 +0,0 @@
// 用于在滚动的过程中,对比上一次渲染的结果和本次需要渲染项
// 确保继续渲染项在新渲染数组中的位置和旧渲染数组中的位置不发生改变
export default class ItemMap<T>{
// 不要用 indexOf 进行位置计算,它会遍历数组
private lastRenderItemToIndexMap: Map<T, number>;
constructor(){
this.lastRenderItemToIndexMap = new Map();
}
public calculateReSortedItems(nextItems: T[]): (T|undefined)[] {
if (this.lastRenderItemToIndexMap.size === 0) {
nextItems.forEach((item, index) => {
this.lastRenderItemToIndexMap.set(item, index);
});
return nextItems;
}
const nextRenderItems: T[] = [];
const length = nextItems.length;
const nextRenderItemToIndexMap = new Map<T,number>();
const addItems = [];
// 遍历 nextItems 找到复用 item 和 新增 item
nextItems.forEach(item => {
const lastIndex = this.lastRenderItemToIndexMap.get(item);
// 处理旧 item
if (lastIndex !== undefined) {
// 使用上一次的位置
nextRenderItems[lastIndex] = item;
// 记录位置
nextRenderItemToIndexMap.set(item, lastIndex);
} else {
// 记录新增的 item
addItems.push(item);
}
});
// 处理新增 item
// 翻转数组后面在调用pop时拿到的是最后一个以确保顺序
addItems.reverse();
for(let i = 0; i < length; i++) {
// 优先将新增 item 放置在空位置上
if (!nextRenderItems[i]) {
const item = addItems.pop();
nextRenderItems[i] = item;
nextRenderItemToIndexMap.set(item, i);
}
}
// 剩余新 item 补在数组后面
for(let i = addItems.length - 1; i >= 0; i--) {
const item = addItems[i];
nextRenderItemToIndexMap.set(item, nextRenderItems.length);
nextRenderItems.push(item);
}
// 如果 nextRenderItems 中存在空 index nextItems 已经耗尽,不用处理
// 确保新旧数组中 item 的 index 值不会发生变化
this.lastRenderItemToIndexMap = nextRenderItemToIndexMap;
return nextRenderItems;
}
}

View File

@ -1,11 +0,0 @@
.container {
position: relative;
overflow-y: auto;
height: 100%;
width: 100%;
}
.item {
position: absolute;
width: 100%;
}

View File

@ -1,119 +0,0 @@
// 内部只记录滚动位置状态值
// data 数组更新后不修改滚动位置,
// 只有修改scrollToItem才会修改滚动位置
import { useState, useRef, useEffect, useMemo } from 'horizon';
import styles from './VList.less';
import ItemMap from './ItemMap';
interface IProps<T extends { id: number | string }> {
data: T[],
width: number, // 暂时未用到,当需要支持横向滚动时使用
height: number, // VList 的高度
children: any, // horizon 组件,组件类型是 T
itemHeight: number,
scrollToItem?: T, // 滚动到指定项位置,如果该项在可见区域内,不滚动,如果不在,则滚动到中间位置
onRendered: (renderInfo: renderInfoType<T>) => void;
filter?(data: T): boolean, // false 表示该行不显示
}
export type renderInfoType<T> = {
visibleItems: T[];
};
function parseTranslate<T>(data: T[], itemHeight: number) {
const map = new Map<T, number>();
data.forEach((item, index) => {
map.set(item, index * itemHeight);
});
return map;
}
export function VList<T extends { id: number | string }>(props: IProps<T>) {
const {
data,
height,
children,
itemHeight,
scrollToItem,
onRendered,
} = props;
const [scrollTop, setScrollTop] = useState(Math.max(data.indexOf(scrollToItem), 0) * itemHeight);
const renderInfoRef: { current: renderInfoType<T> } = useRef({
visibleItems: [],
});
// 每个 item 的 translateY 值固定不变
const itemToTranslateYMap = useMemo(() => parseTranslate(data, itemHeight), [data]);
const itemIndexMap = useMemo(() => new ItemMap<T>(), []);
const containerRef = useRef<HTMLDivElement>();
useEffect(() => {
onRendered(renderInfoRef.current);
});
useEffect(() => {
if (scrollToItem) {
const renderInfo = renderInfoRef.current;
// 在显示区域,不滚动
if (!renderInfo.visibleItems.includes(scrollToItem)) {
const index = data.indexOf(scrollToItem);
// 显示在页面中间
const top = Math.max(index * itemHeight - height / 2, 0);
containerRef.current.scrollTo({ top: top });
}
}
}, [scrollToItem]);
// 滚动事件会频繁触发,通过框架提供的代理会有大量计算寻找 dom 元素。
// 直接绑定到原生事件上减少计算量
useEffect(() => {
const handleScroll = (event: any) => {
const scrollTop = event.target.scrollTop;
setScrollTop(scrollTop);
};
const container = containerRef.current;
container.addEventListener('scroll', handleScroll);
return () => {
container.removeEventListener('scroll', handleScroll);
};
}, []);
const totalHeight = itemHeight * data.length;
const maxIndex = data.length; // slice 截取渲染 item 数组时最大位置不能超过自身长度
// 第一个可见 item index
const firstInViewItemIndex = Math.floor(scrollTop / itemHeight);
// 可见区域前最多冗余 4 个 item
const startRenderIndex = Math.max(firstInViewItemIndex - 4, 0); // index 不能小于0
// 最多可见数量
const maxInViewCount = Math.floor(height / itemHeight);
// 最后可见item index
const lastInViewIndex = Math.min(firstInViewItemIndex + maxInViewCount, maxIndex);
// 记录可见 items
renderInfoRef.current.visibleItems = data.slice(firstInViewItemIndex, lastInViewIndex);
// 可见区域后冗余 4 个 item
const lastRenderIndex = Math.min(lastInViewIndex + 4, maxIndex);
// 需要渲染的 items
const renderItems = data.slice(startRenderIndex, lastRenderIndex);
// 给 items 重新排序,确保未移出渲染数组的 item 在新的渲染数组中位置不变
// 这样在diff算法比较后这部分的 dom 不会发生更新
const nextRenderList = itemIndexMap.calculateReSortedItems(renderItems);
const list = nextRenderList.map((item, i) => {
if (!item) {
return null;
}
return (
<div
key={String(i)} // 固定 key 值,这样就只会更新 translateY 的值
className={styles.item}
style={{ transform: `translateY(${itemToTranslateYMap.get(item)}px)` }} >
{children(item)}
</div>
);
});
return (
<div ref={containerRef} className={styles.container}>
{list}
<div style={{ marginTop: totalHeight }} />
</div>
);
}

View File

@ -1,2 +0,0 @@
export { VList } from './VList';
export type { renderInfoType } from './VList';

View File

@ -1,38 +0,0 @@
@import 'assets.less';
.treeContainer {
height: 100%;
.treeItem {
width: 100%;
position: absolute;
line-height: 1.125rem;
&:hover {
background-color: @select-color;
}
.treeIcon {
color: @arrow-color;
display: inline-block;
width: 12px;
padding-left: 0.5rem;
}
.componentName {
color: @component-name-color;
}
.componentKeyName {
color: @component-key-color;
}
.componentKeyValue {
color: @componentKeyValue-color;
}
}
.select {
background-color: rgb(141 199 248 / 60%);
}
}

View File

@ -1,201 +0,0 @@
import { useState, useEffect } from 'horizon';
import styles from './VTree.less';
import Triangle from '../svgs/Triangle';
import { createRegExp } from '../utils/regExpUtils';
import { SizeObserver } from './SizeObserver';
import { renderInfoType, VList } from './VList';
export interface IData {
id: number;
name: string;
indentation: number;
userKey: string;
}
interface IItem {
hasChild: boolean,
onCollapse: (data: IData) => void,
onClick: (id: IData) => void,
isCollapsed: boolean,
isSelect: boolean,
highlightValue: string,
data: IData,
}
const indentationLength = 20;
function Item(props: IItem) {
const {
hasChild,
onCollapse,
isCollapsed,
data,
onClick,
isSelect,
highlightValue = '',
} = props;
const {
name,
userKey,
indentation,
} = data;
const isShowKey = userKey !== '';
const showIcon = hasChild ? <Triangle director={isCollapsed ? 'right' : 'down'} /> : '';
const handleClickCollapse = () => {
onCollapse(data);
};
const handleClick = () => {
onClick(data);
};
const itemAttr: any = { className: styles.treeItem, onClick: handleClick };
if (isSelect) {
itemAttr.tabIndex = 0;
itemAttr.className = styles.treeItem + ' ' + styles.select;
}
const reg = createRegExp(highlightValue);
const heightCharacters = name.match(reg);
let showName;
if (heightCharacters) {
let cutName = name;
showName = [];
// 高亮第一次匹配即可
const char = heightCharacters[0];
const index = name.search(char);
const notHighlightStr = cutName.slice(0, index);
showName.push(notHighlightStr);
showName.push(<mark>{char}</mark>);
cutName = cutName.slice(index + char.length);
showName.push(cutName);
} else {
showName = name;
}
return (
<div {...itemAttr}>
<div style={{ marginLeft: indentation * indentationLength }} className={styles.treeIcon} onClick={handleClickCollapse} >
{showIcon}
</div>
<span className={styles.componentName} >
{showName}
</span>
{isShowKey && (
<>
<span className={styles.componentKeyName}>
{' '}key
</span>
{'="'}
<span className={styles.componentKeyValue}>
{userKey}
</span>
{'"'}
</>
)}
</div>
);
}
function VTree(props: {
data: IData[],
highlightValue: string,
scrollToItem: IData,
onRendered: (renderInfo: renderInfoType<IData>) => void,
collapsedNodes?: IData[],
onCollapseNode?: (item: IData[]) => void,
selectItem: IData,
onSelectItem: (item: IData) => void,
}) {
const { data, highlightValue, scrollToItem, onRendered, onCollapseNode, onSelectItem } = props;
const [collapseNode, setCollapseNode] = useState(props.collapsedNodes || []);
const [selectItem, setSelectItem] = useState(props.selectItem);
useEffect(() => {
setSelectItem(scrollToItem);
}, [scrollToItem]);
useEffect(() => {
if (props.selectItem !== selectItem) {
setSelectItem(props.selectItem);
}
}, [props.selectItem]);
useEffect(() => {
setCollapseNode(props.collapsedNodes || []);
}, [props.collapsedNodes]);
const changeCollapseNode = (item: IData) => {
const nodes: IData[] = [...collapseNode];
const index = nodes.indexOf(item);
if (index === -1) {
nodes.push(item);
} else {
nodes.splice(index, 1);
}
setCollapseNode(nodes);
if (onCollapseNode) {
onCollapseNode(nodes);
}
};
const handleClickItem = (item: IData) => {
setSelectItem(item);
if (onSelectItem) {
onSelectItem(item);
}
};
let currentCollapseIndentation: null | number = null;
// 过滤掉折叠的 item不展示在 VList 中
const filter = (item: IData) => {
if (currentCollapseIndentation !== null) {
// 缩进更大,不显示
if (item.indentation > currentCollapseIndentation) {
return false;
} else {
// 缩进小,说明完成了该收起节点的子节点处理。
currentCollapseIndentation = null;
}
}
const isCollapsed = collapseNode.includes(item);
if (isCollapsed) {
// 该节点需要收起子节点
currentCollapseIndentation = item.indentation;
}
return true;
};
const showList = data.filter(filter);
return (
<SizeObserver className={styles.treeContainer}>
{(width: number, height: number) => {
return (
<VList
data={showList}
width={width}
height={height}
itemHeight={18}
scrollToItem={selectItem}
onRendered={onRendered}
>
{(item: IData) => {
const isCollapsed = collapseNode.includes(item);
const index = showList.indexOf(item);
// 如果收起,一定有 child
// 不收起场景,如果存在下一个节点,并且节点缩进比自己大,说明下个节点是子节点,节点本身需要显示展开收起图标
const hasChild = isCollapsed || (showList[index + 1]?.indentation > item.indentation);
return (
<Item
hasChild={hasChild}
isCollapsed={collapseNode.includes(item)}
isSelect={selectItem === item}
onCollapse={changeCollapseNode}
onClick={handleClickItem}
highlightValue={highlightValue}
data={item} />
);
}}
</VList>
);
}}
</SizeObserver>
);
}
export default VTree;

View File

@ -1,15 +0,0 @@
@arrow-color: rgb(95, 99, 104);
@divider-color: rgb(202, 205, 209);
@attr-name-color: rgb(200, 0, 0);
@component-name-color: rgb(136, 18, 128);
@component-key-color: rgb(153, 69, 0);
@componentKeyValue-color: rgb(26, 26, 166);
@component-attr-color: rgb(200, 0, 0);
@select-color: rgb(141 199 248 / 60%);
@hover-color: black;
@top-height: 2.625rem;
@divider-width: 0.2px;
@common-font-size: 12px;
@divider-style: @divider-color solid @divider-width;

View File

@ -1,38 +0,0 @@
import { injectCode } from '../utils/injectUtils';
import { checkMessage } from '../utils/transferTool';
import { DevToolContentScript, DevToolHook, DevToolBackground } from './../utils/constants';
import { changeSource } from './../utils/transferTool';
// 页面的window对象不能直接通过 contentScript 代码修改,只能通过添加 js 代码往页面 window 注入hook
injectCode(chrome.runtime.getURL('/injector.js'));
// 监听来自页面的信息
window.addEventListener('message', event => {
// 只监听来自本页面的消息
if (event.source !== window) {
return;
}
const data = event.data;
if (checkMessage(data, DevToolHook)) {
changeSource(data, DevToolContentScript);
// 传递给background
chrome.runtime.sendMessage(data);
}
}, false);
// 监听来自background的消息
chrome.runtime.onMessage.addListener(
function (message, sender, sendResponse) {
// 该方法可以监听页面 contentScript 和插件的消息
// 没有 tab 信息说明消息来自插件
if (!sender.tab && checkMessage(message, DevToolBackground)) {
changeSource(message, DevToolContentScript);
// 传递消息给页面
window.postMessage(message, '*');
}
sendResponse({status: 'ok'});
}
);

View File

@ -1,99 +0,0 @@
/**
* tree VNode
* getMockVNodeTree
*/
import { parseAttr } from '../parser/parseAttr';
import parseTreeRoot from '../parser/parseVNode';
import { VNode } from '../../../horizon/src/renderer/vnode/VNode';
import { FunctionComponent, ClassComponent } from '../../../horizon/src/renderer/vnode/VNodeTags';
import { helper } from '../../../horizon/src/external/devtools';
const mockComponentNames = ['Apple', 'Pear', 'Banana', 'Orange', 'Jenny', 'Kiwi', 'Coconut'];
function MockVNode(tag: string, props = {}, key = null, realNode = {}) {
const vNode = new VNode(tag, props, key, realNode);
const name = mockComponentNames.shift() || 'MockComponent';
vNode.type = { name };
return vNode;
}
interface IMockTree {
tag: string,
children?: IMockTree[],
}
// 模拟树
const tree: IMockTree = {
tag: ClassComponent,
children: [
{ tag: FunctionComponent },
{ tag: ClassComponent },
{ tag: FunctionComponent },
{
tag: FunctionComponent,
children: [
{ tag: ClassComponent }
]
}
]
};
function addOneThousandNode(node: IMockTree) {
const nodes = [];
for (let i = 0; i < 1000; i++) {
nodes.push({ tag: FunctionComponent });
}
node?.children.push({ tag: ClassComponent, children: nodes });
}
addOneThousandNode(tree);
/**
* mock数据转变为 VNode
*
* @param node
* @param vNode VNode节点
*/
function getMockVNodeTree(node: IMockTree, vNode: VNode) {
const children = node.children;
if (children && children.length !== 0) {
const childNode = children[0];
let childVNode = MockVNode(childNode.tag);
childVNode.key = '0';
getMockVNodeTree(childNode, childVNode);
// 需要建立双链
vNode.child = childVNode;
childVNode.parent = vNode;
for (let i = 1; i < children.length; i++) {
const nextNode = children[i];
const nextVNode = MockVNode(nextNode.tag);
nextVNode.key = String(i);
nextVNode.parent = vNode;
getMockVNodeTree(nextNode, nextVNode);
childVNode.next = nextVNode;
childVNode = nextVNode;
}
}
}
const rootVNode = MockVNode(tree.tag);
getMockVNodeTree(tree, rootVNode);
export const mockParsedVNodeData = parseTreeRoot(helper.travelVNodeTree, rootVNode);
const mockState = {
str: 'jenny',
num: 3,
boolean: true,
und: undefined,
fun: () => ({}),
symbol: Symbol('sym'),
map: new Map([['a', 'a']]),
set: new Set(['a', 1, 2, Symbol('bambi')]),
arr: [1, 2, 3, 4],
obj: {
niko: { jenny: 'jenny' }
}
};
export const parsedMockState = parseAttr(mockState);

View File

@ -1,22 +0,0 @@
import { Component } from 'horizon';
const defaultState = {
name: 'jenny',
boolean: true,
};
export default class MockClassComponent extends Component<{fruit: string}, typeof defaultState> {
state = defaultState;
render() {
return (
<div>
<button onClick={() => (this.setState({name: 'pika'}))} >update state</button>
{this.state.name}
{this.props?.fruit}
</div>
);
}
}

View File

@ -1,3 +0,0 @@
import { createContext } from 'horizon';
export const MockContext = createContext({value: 'default context value'});

View File

@ -1,38 +0,0 @@
import { useState, useEffect, useRef, useContext, useReducer } from 'horizon';
import { MockContext } from './MockContext';
const initialState = {count: 0};
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
default:
throw new Error();
}
}
export default function MockFunctionComponent(props) {
const [state, dispatch] = useReducer(reducer, initialState);
const [age, setAge] = useState(0);
const [name, setName] = useState({test: 1});
const domRef = useRef<HTMLDivElement>();
const objRef = useRef({ str: 'string' });
const context = useContext(MockContext);
useEffect(() => { }, []);
return (
<div>
age: {age}
name: {name.test}
<button onClick={() => setAge(age + 1)} >update age</button>
count: {props.count}
<div ref={domRef} />
<div>{objRef.current.str}</div>
<div>{context.ctx}</div>
</div>
);
}

View File

@ -1,22 +0,0 @@
import { render, useState } from 'horizon';
import MockClassComponent from './MockClassComponent';
import MockFunctionComponent from './MockFunctionComponent';
import { MockContext } from './MockContext';
const root = document.createElement('div');
document.body.append(root);
function App() {
const [count, setCount] = useState(12);
return (
<div>
<button onClick={() => (setCount(count + 1))} >add count</button>
<MockContext.Provider value={{ ctx: 'I am ctx' }}>
<MockClassComponent fruit={'apple'} />
<MockFunctionComponent count={count}/>
</MockContext.Provider>
abc
</div>
);
}
render(<App />, root);

View File

@ -1,28 +0,0 @@
<!doctype html>
<html>
<head>
<meta charset="utf8">
<title>Horizon Mock Page</title>
<script src="horizon.production.js"></script>
<style>
html {
width: 100%;
height: 100%;
}
body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
}
</style>
</head>
<body>
</body>
</html>

View File

@ -1,147 +0,0 @@
// 过滤树的抽象逻辑
// 需要知道渲染了哪些数据,过滤的字符串/正则表达式
// 控制Tree组件位置跳转告知匹配结果
// 清空搜索框,告知搜索框当前是第几个结果,跳转搜索结果
//
// 跳转搜索结果的交互逻辑:
// 如果当前页面存在匹配项,页面不动
// 如果当前页面不存在匹配项,页面跳转到第一个匹配项位置
// 如果匹配项被折叠,需要展开其父节点。注意只展开当前匹配项的父节点,其他匹配项的父节点不展开
// 跳转到上一个匹配项或下一个匹配项时,如果匹配项被折叠,需要展开其父节点
//
// 寻找父节点:
// 找到该节点的缩进值和index值在data中向上遍历通过缩进值判断父节点
import { useState, useRef } from 'horizon';
import { createRegExp } from '../utils/regExpUtils';
/**
*
*
* @param item
* @param data
* @param collapsedNodes
* @returns
*/
function expandItemParent(item: BaseType, data: BaseType[], collapsedNodes: BaseType[]): BaseType[] {
const index = data.indexOf(item);
let currentIndentation = item.indentation;
// 不对原始数据进行修改
const newCollapsedNodes = [...collapsedNodes];
for (let i = index - 1; i >= 0; i--) {
const lastData = data[i];
const lastIndentation = lastData.indentation;
// 缩进更小,找到了父节点
if (lastIndentation < currentIndentation) {
// 更新缩进值,只找父节点的父节点,避免修改父节点的兄弟节点的展开状态
currentIndentation = lastIndentation;
const cIndex = newCollapsedNodes.indexOf(lastData);
if (cIndex !== -1) {
newCollapsedNodes.splice(cIndex, 1);
}
}
}
return newCollapsedNodes;
}
type BaseType = {
id: string,
name: string,
indentation: number,
}
export function FilterTree<T extends BaseType>(props: { data: T[] }) {
const { data } = props;
const [filterValue, setFilterValue] = useState('');
const [currentItem, setCurrentItem] = useState(null); // 当前选中的匹配项
const showItemsRef = useRef([]); // 页面展示的 items
const matchItemsRef = useRef([]); // 匹配过滤条件的 items
const collapsedNodesRef = useRef([]); // 折叠节点,如果匹配 item 被折叠了,需要展开
const matchItems = matchItemsRef.current;
const collapsedNodes = collapsedNodesRef.current;
const updateCollapsedNodes = (item: BaseType) => {
const newCollapsedNodes = expandItemParent(item, data, collapsedNodes);
// 如果新旧收起节点数组长度不一样,说明存在收起节点
if (newCollapsedNodes.length !== collapsedNodes.length) {
// 更新引用,确保 VTree 拿到新的 collapsedNodes
collapsedNodesRef.current = newCollapsedNodes;
}
};
const onChangeSearchValue = (search: string) => {
const reg = createRegExp(search);
let newCurrentItem = null;
let newMatchItems = [];
if (search !== '') {
const showItems: T[] = showItemsRef.current;
newMatchItems = data.reduce((pre, current) => {
const { name } = current;
if (reg && name.match(reg)) {
pre.push(current);
// 如果当前页面显示的 item 存在匹配项,则把它设置为 currentItem
if (newCurrentItem === null && showItems.includes(current)) {
newCurrentItem = current;
}
}
return pre;
}, []);
if (newMatchItems.length === 0) {
setCurrentItem(null);
} else {
if (newCurrentItem === null) {
const item = newMatchItems[0];
// 不处于当前展示页面,需要展开父节点
updateCollapsedNodes(item);
setCurrentItem(item);
} else {
setCurrentItem(newCurrentItem);
}
}
} else {
setCurrentItem(null);
}
matchItemsRef.current = newMatchItems;
setFilterValue(search);
};
const onSelectNext = () => {
const index = matchItems.indexOf(currentItem);
const nextIndex = index + 1;
const item = nextIndex < matchItemsRef.current.length ? matchItems[nextIndex] : matchItems[0];
// 可能不处于当前展示页面,需要展开父节点
updateCollapsedNodes(item);
setCurrentItem(item);
};
const onSelectLast = () => {
const index = matchItems.indexOf(currentItem);
const last = index - 1;
const item = last >= 0 ? matchItems[last] : matchItems[matchItems.length - 1];
// 可能不处于当前展示页面,需要展开父节点
updateCollapsedNodes(item);
setCurrentItem(item);
};
const setShowItems = (items) => {
showItemsRef.current = [...items];
};
const onClear = () => {
onChangeSearchValue('');
};
const setCollapsedNodes = (items) => {
// 不更新引用,避免子组件的重复渲染
collapsedNodesRef.current.length = 0;
collapsedNodesRef.current.push(...items);
};
return {
filterValue,
onChangeSearchValue,
onClear,
currentItem,
matchItems,
onSelectNext,
onSelectLast,
setShowItems,
collapsedNodes,
setCollapsedNodes,
};
}

View File

@ -1,180 +0,0 @@
import parseTreeRoot, { clearVNode, queryVNode } from '../parser/parseVNode';
import { packagePayload, checkMessage } from '../utils/transferTool';
import {
RequestAllVNodeTreeInfos,
AllVNodeTreesInfos,
RequestComponentAttrs,
ComponentAttrs,
DevToolHook,
DevToolContentScript,
ModifyAttrs,
ModifyHooks,
ModifyState,
ModifyProps,
InspectDom,
LogComponentData
} from '../utils/constants';
import { VNode } from '../../../horizon/src/renderer/vnode/VNode';
import { parseVNodeAttrs } from '../parser/parseAttr';
const roots = [];
function addIfNotInclude(treeRoot: VNode) {
if (!roots.includes(treeRoot)) {
roots.push(treeRoot);
}
}
function send() {
const result = roots.reduce((pre, current) => {
const info = parseTreeRoot(helper.travelVNodeTree ,current);
pre.push(info);
return pre;
}, []);
postMessage(AllVNodeTreesInfos, result);
}
function deleteVNode(vNode: VNode) {
// 开发工具中保存了 vNode 的引用,在清理 VNode 的时候需要一并删除
clearVNode(vNode);
const index = roots.indexOf(vNode);
if (index !== -1) {
roots.splice(index, 1);
}
}
function postMessage(type: string, data) {
window.postMessage(packagePayload({
type: type,
data: data,
}, DevToolHook), '*');
}
function parseCompAttrs(id: number) {
const vNode = queryVNode(id);
if (!vNode) {
console.error('Do not find match vNode, this is a bug, please report us');
return;
}
const parsedAttrs = parseVNodeAttrs(vNode, helper.getHookInfo);
postMessage(ComponentAttrs, parsedAttrs);
}
function calculateNextValue(editValue, value, attrPath) {
let nextState;
const editValueType = typeof editValue;
if (editValueType === 'string' || editValueType === 'undefined' || editValueType === 'boolean') {
nextState = value;
} else if (editValueType === 'number') {
const numValue = Number(value);
nextState = isNaN(numValue) ? value : numValue; // 如果能转为数字,转数字,不能转数字,用原值
} else if(editValueType === 'object') {
if (editValue === null) {
nextState = value;
} else {
const newValue = Array.isArray(editValue) ? [...editValue] : {...editValue};
// 遍历读取到直接指向需要修改值的对象
let attr = newValue;
for(let i = 0; i < attrPath.length - 1; i++) {
attr = attr[attrPath[i]];
}
// 修改对象上的值
attr[attrPath[attrPath.length - 1]] = value;
nextState = newValue;
}
} else {
console.error('The devTool tried to edit a non-editable value, this is a bug, please report', editValue);
}
return nextState;
}
function modifyVNodeAttrs(data) {
const {type, id, value, path} = data;
const vNode = queryVNode(id);
if (!vNode) {
console.error('Do not find match vNode, this is a bug, please report us');
return;
}
if (type === ModifyProps) {
const nextProps = calculateNextValue(vNode.props, value, path);
helper.updateProps(vNode, nextProps);
} else if (type === ModifyHooks) {
const hooks = vNode.hooks;
const editHook = hooks[path[0]];
const hookInfo = helper.getHookInfo(editHook);
if (hookInfo) {
const editValue = hookInfo.value;
// path 的第一个值指向 hIndex从第二个值才开始指向具体属性访问路径
const nextState = calculateNextValue(editValue, value, path.slice(1));
helper.updateHooks(vNode, path[0], nextState);
} else {
console.error('The devTool tried to edit a non-editable hook, this is a bug, please report', hooks);
}
} else if (type === ModifyState) {
const oldState = vNode.state || {};
const nextState = {...oldState};
let accessRef = nextState;
for(let i = 0; i < path.length - 1; i++) {
accessRef = accessRef[path[i]];
}
accessRef[path[path.length - 1]] = value;
helper.updateState(vNode, nextState);
}
}
function logComponentData(id: number) {
const vNode = queryVNode(id);
if (vNode) {
const info = helper.getComponentInfo(vNode);
console.log('Component Info: ', info);
}
}
let helper;
function init(horizonHelper) {
helper = horizonHelper;
window.__HORIZON_DEV_HOOK__.isInit = true;
}
function injectHook() {
if (window.__HORIZON_DEV_HOOK__) {
return;
}
Object.defineProperty(window, '__HORIZON_DEV_HOOK__', {
enumerable: false,
value: {
init,
isInit: false,
addIfNotInclude,
send,
deleteVNode,
},
});
window.addEventListener('message', function (event) {
// We only accept messages from ourselves
if (event.source !== window) {
return;
}
const request = event.data;
if (checkMessage(request, DevToolContentScript)) {
const { payload } = request;
const { type, data } = payload;
if (type === RequestAllVNodeTreeInfos) {
send();
} else if (type === RequestComponentAttrs) {
parseCompAttrs(data);
} else if (type === ModifyAttrs) {
modifyVNodeAttrs(data);
} else if (type === InspectDom) {
console.log(data);
} else if (type === LogComponentData) {
logComponentData(data);
} else {
console.warn('unknown command', type);
}
}
});
}
injectHook();

View File

@ -1,7 +0,0 @@
chrome.devtools.panels.create('Horizon',
'',
'panel.html',
function(panel) {
}
);

View File

@ -1,12 +0,0 @@
<!DOCTYPE html>
<html>
<head>
</head>
<body>
</div>
<div>
<p>Horizon dev tools!</p>
</div>
</body>
<script src="main.js"></script>
</html>

View File

@ -1,27 +0,0 @@
{
"name": "Horizon dev tool",
"description": "Horizon chrome dev extension",
"version": "1.0",
"minimum_chrome_version": "10.0",
"manifest_version": 3,
"background": {
"service_worker": "background.js"
},
"permissions": ["storage", "activeTab", "scripting"],
"devtools_page": "main.html",
"action": {},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["contentScript.js"],
"run_at": "document_start"
}
],
"web_accessible_resources": [
{
"resources": [ "injector.js", "background.js" ],
"matches": ["<all_urls>"]
}
]
}

View File

@ -1,74 +0,0 @@
@import '../components/assets.less';
.app {
display: flex;
flex-direction: row;
height: 100%;
font-size: @common-font-size;
}
button {
border: none;
background: none;
padding: 0;
}
.left {
flex: 7;
display: flex;
flex-direction: column;
.left_top {
border-bottom: @divider-style;
flex: 0 0 @top-height;
display: flex;
align-items: center;
padding-right: 0.4rem;
.select {
padding: 0 0.25rem 0 0.25rem;
flex: 0 0;
}
.divider {
flex: 0 0 1px;
margin: 0 0.25rem 0 0.25rem;
border-left: @divider-style;
height: calc(100% - 1rem);
}
.search {
flex: 1 1 0;
}
.searchResult{
flex: 0 0 ;
padding: 0 0.4rem;
}
.searchAction {
flex: 0 0 1rem;
height: 1rem;
color: @arrow-color;
&:hover{
color: @hover-color;
}
}
}
.left_bottom {
flex: 1;
height: 0;
}
}
.right {
flex: 3;
border-left: @divider-style;
}
input {
outline: none;
border-width: 0;
padding: 0;
}

View File

@ -1,222 +0,0 @@
import { useState, useEffect, useRef } from 'horizon';
import VTree, { IData } from '../components/VTree';
import Search from '../components/Search';
import ComponentInfo from '../components/ComponentInfo';
import styles from './App.less';
import Select from '../svgs/Select';
import { mockParsedVNodeData, parsedMockState } from '../devtools/mock';
import { FilterTree } from '../hooks/FilterTree';
import Close from '../svgs/Close';
import Arrow from './../svgs/Arrow';
import {
AllVNodeTreesInfos,
RequestComponentAttrs,
ComponentAttrs,
} from '../utils/constants';
import {
addBackgroundMessageListener,
initBackgroundConnection,
postMessageToBackground, removeBackgroundMessageListener,
} from '../panelConnection';
import { IAttr } from '../parser/parseAttr';
import { createLogger } from '../utils/logUtil';
const logger = createLogger('panelApp');
const parseVNodeData = (rawData, idToTreeNodeMap , nextIdToTreeNodeMap) => {
const idIndentationMap: {
[id: string]: number;
} = {};
const data: IData[] = [];
let i = 0;
while (i < rawData.length) {
const id = rawData[i] as number;
i++;
const name = rawData[i] as string;
i++;
const parentId = rawData[i] as string;
i++;
const userKey = rawData[i] as string;
i++;
const indentation = parentId === '' ? 0 : idIndentationMap[parentId] + 1;
idIndentationMap[id] = indentation;
const lastItem = idToTreeNodeMap[id];
if (lastItem) {
// 由于 diff 算法限制,一个 vNode 的 nameuserKeyindentation 属性不会发生变化
// 但是在跳转到新页面时id 值重置,此时原有 id 对应的节点都发生了变化,需要更新
// 为了让架构尽可能简单,我们不区分是否是页面跳转,所以每次都需要重新赋值
nextIdToTreeNodeMap[id] = lastItem;
lastItem.name = name;
lastItem.indentation = indentation;
lastItem.userKey = userKey;
data.push(lastItem);
} else {
const item = {
id, name, indentation, userKey
};
nextIdToTreeNodeMap[id] = item;
data.push(item);
}
}
return data;
};
const getParents = (item: IData | null, parsedVNodeData: IData[]) => {
const parents: IData[] = [];
if (item) {
const index = parsedVNodeData.indexOf(item);
let indentation = item.indentation;
for (let i = index; i >= 0; i--) {
const last = parsedVNodeData[i];
const lastIndentation = last.indentation;
if (lastIndentation < indentation) {
parents.push(last);
indentation = lastIndentation;
}
}
}
return parents;
};
interface IIdToNodeMap {
[id: number]: IData;
}
function App() {
const [parsedVNodeData, setParsedVNodeData] = useState([]);
const [componentAttrs, setComponentAttrs] = useState<{
parsedProps?: IAttr[],
parsedState?: IAttr[],
parsedHooks?: IAttr[],
}>({});
const [selectComp, setSelectComp] = useState(null);
const idToTreeNodeMapRef = useRef<IIdToNodeMap>({});
const {
filterValue,
onChangeSearchValue: setFilterValue,
onClear,
currentItem,
matchItems,
onSelectNext,
onSelectLast,
setShowItems,
collapsedNodes,
setCollapsedNodes,
} = FilterTree({ data: parsedVNodeData });
useEffect(() => {
if (isDev) {
const nextIdToTreeNodeMap: IIdToNodeMap = {};
const parsedData = parseVNodeData(mockParsedVNodeData, idToTreeNodeMapRef.current, nextIdToTreeNodeMap);
idToTreeNodeMapRef.current = nextIdToTreeNodeMap;
setParsedVNodeData(parsedData);
setComponentAttrs({
parsedProps: parsedMockState,
parsedState: parsedMockState,
});
} else {
const handleBackgroundMessage = (message) => {
const { payload } = message;
// 对象数据只是记录了引用,内容可能在后续被修改,打印字符串可以获取当前真正内容,不被后续修改影响
logger.info(JSON.stringify(payload));
if (payload) {
const { type, data } = payload;
if (type === AllVNodeTreesInfos) {
const idToTreeNodeMap = idToTreeNodeMapRef.current;
const nextIdToTreeNodeMap: IIdToNodeMap = {};
const allTreeData = data.reduce((pre, current) => {
const parsedTreeData = parseVNodeData(current, idToTreeNodeMap, nextIdToTreeNodeMap);
return pre.concat(parsedTreeData);
}, []);
idToTreeNodeMapRef.current = nextIdToTreeNodeMap;
setParsedVNodeData(allTreeData);
} else if (type === ComponentAttrs) {
const {parsedProps, parsedState, parsedHooks} = data;
setComponentAttrs({
parsedProps,
parsedState,
parsedHooks,
});
}
}
};
// 在页面渲染后初始化连接
initBackgroundConnection();
// 监听 background消息
addBackgroundMessageListener(handleBackgroundMessage);
return () => {
removeBackgroundMessageListener(handleBackgroundMessage);
};
}
}, []);
const handleSearchChange = (str: string) => {
setFilterValue(str);
};
const handleSelectComp = (item: IData) => {
if (isDev) {
setComponentAttrs({
parsedProps: parsedMockState,
parsedState: parsedMockState,
});
} else {
postMessageToBackground(RequestComponentAttrs, item.id);
}
setSelectComp(item);
};
const handleClickParent = (item: IData) => {
setSelectComp(item);
};
const onRendered = (info) => {
setShowItems(info.visibleItems);
};
const parents = getParents(selectComp, parsedVNodeData);
return (
<div className={styles.app}>
<div className={styles.left}>
<div className={styles.left_top} >
<div className={styles.select} >
<Select />
</div>
<div className={styles.divider} />
<div className={styles.search}>
<Search onChange={handleSearchChange} value={filterValue} />
</div>
{filterValue !== '' && <>
<span className={styles.searchResult}>{`${matchItems.indexOf(currentItem) + 1}/${matchItems.length}`}</span>
<div className={styles.divider} />
<button className={styles.searchAction} onClick={onSelectLast}><Arrow direction={'up'} /></button>
<button className={styles.searchAction} onClick={onSelectNext}><Arrow direction={'down'} /></button>
<button className={styles.searchAction} onClick={onClear}><Close /></button>
</>}
</div>
<div className={styles.left_bottom}>
<VTree
data={parsedVNodeData}
highlightValue={filterValue}
onRendered={onRendered}
collapsedNodes={collapsedNodes}
onCollapseNode={setCollapsedNodes}
scrollToItem={currentItem}
selectItem={selectComp}
onSelectItem={handleSelectComp} />
</div>
</div>
<div className={styles.right}>
<ComponentInfo
name={selectComp ? selectComp.name : null}
attrs={selectComp ? componentAttrs : {}}
parents={parents}
id={selectComp ? selectComp.id : null}
onClickParent={handleClickParent} />
</div>
</div>
);
}
export default App;

View File

@ -1,7 +0,0 @@
import { render } from 'horizon';
import App from './App';
render(
<App />,
document.getElementById('root')
);

View File

@ -1,33 +0,0 @@
<!doctype html>
<html style="display: flex">
<head>
<meta charset="utf8">
<style>
html {
width: 100%;
height: 100%;
}
body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
}
#root {
width: 100%;
height: 100%;
}
</style>
<script src='horizon.development.js'></script>
</head>
<body>
<div id="root"></div>
<script src="panel.js"></script>
</body>
</html>

View File

@ -1,61 +0,0 @@
import { packagePayload } from '../utils/transferTool';
import { DevToolPanel, InitDevToolPageConnection } from '../utils/constants';
let connection;
const callbacks = [];
export function addBackgroundMessageListener(fun: (message) => void) {
callbacks.push(fun);
}
export function removeBackgroundMessageListener(fun: (message) => void) {
const index = callbacks.indexOf(fun);
if (index !== -1) {
callbacks.splice(index, 1);
}
}
export function initBackgroundConnection() {
console.log(!isDev);
if (!isDev) {
try {
connection = chrome.runtime.connect({ name: 'panel' });
const notice = message => {
callbacks.forEach(fun => {
fun(message);
});
};
// TODO: 我们需要删除 notice 吗?如果需要,在什么时候删除
// 监听 background 消息
connection.onMessage.addListener(notice);
// 页面打开后发送初始化请求
postMessageToBackground(InitDevToolPageConnection);
} catch (e) {
console.error('create connection failed');
console.error(e);
}
}
}
let reconnectTimes = 0;
export function postMessageToBackground(type: string, data?: any) {
try{
const payLoad = data
? { type, tabId: chrome.devtools.inspectedWindow.tabId, data }
: { type, tabId: chrome.devtools.inspectedWindow.tabId };
connection.postMessage(packagePayload(payLoad, DevToolPanel));
} catch(err) {
// 可能出现 port 关闭的场景,需要重新建立连接,增加可靠性
if (reconnectTimes === 20) {
reconnectTimes = 0;
console.error('reconnect failed');
return;
}
console.error(err);
reconnectTimes++;
// 重建连接
initBackgroundConnection();
// 初始化成功后才会重新发送消息
postMessageToBackground(type, data);
}
}

View File

@ -1 +0,0 @@
export * from './PanelConnection';

View File

@ -1,199 +0,0 @@
import { Hook } from '../../../horizon/src/renderer/hooks/HookType';
import { ModifyHooks, ModifyProps, ModifyState } from '../utils/constants';
import { VNode } from '../../../horizon/src/renderer/vnode/VNode';
import { ClassComponent, FunctionComponent } from '../../../horizon/src/renderer/vnode/VNodeTags';
// 展示值为 string 的可编辑类型
type editableStringType = 'string' | 'number' | 'undefined' | 'null';
// 展示值为 string 的不可编辑类型
type unEditableStringType = 'function' | 'symbol' | 'object' | 'map' | 'set' | 'array'
| 'dom' // 值为 dom 元素的 ref 类型
| 'ref'; // 值为其他数据的 ref 类型
type showAsStringType = editableStringType | unEditableStringType;
export type IAttr = {
name: string | number;
indentation: number;
hIndex?: number; // 用于记录 hook 的 hIndex 值
} & ({
type: showAsStringType;
value: string;
} | {
type: 'boolean';
value: boolean;
})
type showType = showAsStringType | 'boolean';
const parseSubAttr = (
attr: any,
parentIndentation: number,
attrName: string,
result: IAttr[],
hIndex?: number) => {
const attrType = typeof attr;
let value: any;
let showType: showType;
let addSubState;
if (attrType === 'boolean' ||
attrType === 'number' ||
attrType === 'string' ||
attrType === 'undefined') {
value = attr;
showType = attrType;
} else if (attrType === 'function') {
const funName = attr.name;
value = `f() ${funName}{}`;
} else if (attrType === 'symbol') {
value = attr.description;
} else if (attrType === 'object') {
if (attr === null) {
showType = 'null';
} else if (attr instanceof Map) {
showType = 'map';
const size = attr.size;
value = `Map(${size})`;
addSubState = () => {
attr.forEach((value, key) => {
parseSubAttr(value, parentIndentation + 2, key, result);
});
};
} else if (attr instanceof Set) {
showType = 'set';
const size = attr.size;
value = `Set(${size})`;
addSubState = () => {
let i = 0;
attr.forEach((value) => {
parseSubAttr(value, parentIndentation + 2, String(i), result);
});
i++;
};
} else if (Array.isArray(attr)) {
showType = 'array';
value = `Array(${attr.length})`;
addSubState = () => {
attr.forEach((value, index) => {
parseSubAttr(value, parentIndentation + 2, String(index), result);
});
};
} else if (attr instanceof Element) {
showType = 'dom';
value = attr.tagName;
} else {
showType = attrType;
value = '{...}';
addSubState = () => {
Object.keys(attr).forEach((key) => {
parseSubAttr(attr[key], parentIndentation + 2, key, result);
});
};
}
}
const item: IAttr = {
name: attrName,
type: showType,
value,
indentation: parentIndentation + 1,
};
if (hIndex !== undefined) {
item.hIndex = hIndex;
}
result.push(item);
if (addSubState) {
addSubState();
}
};
// 将属性的值解析成固定格式props 和 类组件的 state 必须是一个对象
export function parseAttr(rootAttr: any) {
const result: IAttr[] = [];
const indentation = 0;
if (typeof rootAttr === 'object' && rootAttr !== null)
Object.keys(rootAttr).forEach(key => {
parseSubAttr(rootAttr[key], indentation, key, result);
});
return result;
}
export function parseHooks(hooks: Hook<any, any>[], getHookInfo) {
const result: IAttr[] = [];
const indentation = 0;
hooks.forEach(hook => {
const hookInfo = getHookInfo(hook);
if (hookInfo) {
const {name, hIndex, value} = hookInfo;
parseSubAttr(value, indentation, name, result, hIndex);
}
});
return result;
}
export function parseVNodeAttrs(vNode: VNode, getHookInfo) {
const tag = vNode.tag;
if (tag === ClassComponent) {
const { props, state } = vNode;
const parsedProps = parseAttr(props);
const parsedState = parseAttr(state);
return {
parsedProps,
parsedState,
};
} else if (tag === FunctionComponent) {
const { props, hooks } = vNode;
const parsedProps = parseAttr(props);
const parsedHooks = parseHooks(hooks, getHookInfo);
return {
parsedProps,
parsedHooks,
};
}
}
// 计算属性的访问顺序
function calculateAttrAccessPath(item: IAttr, index: number, attrs: IAttr[], isHook: boolean) {
let currentIndentation = item.indentation;
const path = [item.name];
let hookRootItem: IAttr = item;
for(let i = index - 1; i >= 0; i--) {
const lastItem = attrs[i];
const lastIndentation = lastItem.indentation;
if (lastIndentation < currentIndentation) {
hookRootItem = lastItem;
path.push(lastItem.name);
currentIndentation = lastIndentation;
}
}
path.reverse();
if (isHook) {
if (hookRootItem) {
path[0] = hookRootItem.hIndex;
} else {
console.error('There is a bug, please report');
}
}
return path;
}
export function buildAttrModifyData(parsedAttrsType: string, attrs: IAttr[], value, item: IAttr, index: number, id: number) {
let type;
if (parsedAttrsType === 'parsedProps') {
type = ModifyProps;
} else if (parsedAttrsType === 'parsedState') {
type = ModifyState;
} else if (parsedAttrsType === 'parsedHooks') {
type = ModifyHooks;
} else {
return null;
}
const path = calculateAttrAccessPath(item, index, attrs, parsedAttrsType === 'parsedHooks');
return {
id: id,
type: type,
value: value,
path: path,
};
}

View File

@ -1,75 +0,0 @@
import { VNode } from '../../../horizon/src/renderer/vnode/VNode';
import { ClassComponent, FunctionComponent } from '../../../horizon/src/renderer/vnode/VNodeTags';
// 建立双向映射关系,当用户在修改属性值后,可以找到对应的 VNode
const VNodeToIdMap = new Map<VNode, number>();
const IdToVNodeMap = new Map<number, VNode>();
let uid = 0;
function generateUid (vNode: VNode) {
const id = VNodeToIdMap.get(vNode);
if (id !== undefined) {
return id;
}
uid++;
return uid;
}
function isUserComponent(tag: string) {
// TODO: 添加其他组件
return tag === ClassComponent || tag === FunctionComponent;
}
function getParentUserComponent(node: VNode) {
let parent = node.parent;
while(parent) {
if (isUserComponent(parent.tag)) {
break;
}
parent = parent.parent;
}
return parent;
}
function parseTreeRoot(travelVNodeTree, treeRoot: VNode) {
const result: any[] = [];
travelVNodeTree(treeRoot, (node: VNode) => {
const tag = node.tag;
if (isUserComponent(tag)) {
const id = generateUid(node);
result.push(id);
const name = node.type.name;
result.push(name);
const parent = getParentUserComponent(node);
if (parent) {
const parentId = VNodeToIdMap.get(parent);
result.push(parentId);
} else {
result.push('');
}
const key = node.key;
if (key !== null) {
result.push(key);
} else {
result.push('');
}
VNodeToIdMap.set(node, id);
IdToVNodeMap.set(id, node);
}
});
return result;
}
export function queryVNode(id: number): VNode|undefined {
return IdToVNodeMap.get(id);
}
export function clearVNode(vNode: VNode) {
if (VNodeToIdMap.has(vNode)) {
const id = VNodeToIdMap.get(vNode);
VNodeToIdMap.delete(vNode);
IdToVNodeMap.delete(id);
}
}
export default parseTreeRoot;

View File

@ -1,17 +0,0 @@
interface IArrow {
direction: 'up' | 'down'
}
export default function Arrow({ direction: director }: IArrow) {
let d: string;
if (director === 'up') {
d = 'M4 9.5 L5 10.5 L8 7.5 L11 10.5 L12 9.5 L8 5.5 z';
} else if (director === 'down') {
d = 'M5 5.5 L4 6.5 L8 10.5 L12 6.5 L11 5.5 L8 8.5z';
}
return (
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' width='1rem' height='1rem'>
<path d={d} fill='currentColor' />
</svg>
);
}

View File

@ -1,8 +0,0 @@
export default function Close() {
return (
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' width='1rem' height='1rem'>
<path d='M4 3 L3 4 L7 8 L3 12 L4 13 L8 9 L12 13 L13 12 L9 8 L13 4 L12 3 L8 7z' fill='currentColor' />
</svg>
);
}

View File

@ -1,8 +0,0 @@
export default function Debug() {
return (
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' width='1rem' height='1rem'>
<path d='m2 0l12 8l-12 8 z' fill='#000'/>
</svg>
);
}

View File

@ -1,10 +0,0 @@
export default function Eye() {
return (
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' width='1rem' height='1rem'>
<ellipse cx="8" cy="8" rx="8" ry="6" />
<circle cx="8" cy="8" r="4" fill="rgb(255, 255, 255)" />
<circle cx="8" cy="8" r="2" fill="#000000" />
</svg>
);
}

View File

@ -1,8 +0,0 @@
export default function Select() {
return (
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' width='1rem' height='1rem'>
<path d='M14 6 V3 C14 2.5 13.5 2 13 2 H3 C2.5 2 2 2.5 2 3 V13 C2 13.5 2.5 14 3 14H6 V13 H3 V3 H13 V6z M7 7 L9 15 L11 12 L14 15 L15 14 L12 11 L15 9z' fill='#000' />
</svg>
);
}

View File

@ -1,17 +0,0 @@
interface IArrow {
director: 'right' | 'down'
}
export default function Triangle({ director }: IArrow) {
let d: string;
if (director === 'right') {
d = 'm2 0l12 8l-12 8 z';
} else if (director === 'down') {
d = 'm0 2h16 l-8 12 z';
}
return (
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' width='8px' height='8px'>
<path d={d} fill='currentColor' />
</svg>
);
}

View File

@ -1,35 +0,0 @@
// panel 页面打开后初始化连接标志
export const InitDevToolPageConnection = 'init dev tool page connection';
// background 解析全部 root VNodes 标志
export const RequestAllVNodeTreeInfos = 'request all vNodes tree infos';
// vNodes 全部树解析结果标志
export const AllVNodeTreesInfos = 'vNode trees Infos';
// 一棵树的解析
export const OneVNodeTreeInfos = 'one vNode tree';
// 获取组件属性
export const RequestComponentAttrs = 'get component attrs';
// 返回组件属性
export const ComponentAttrs = 'component attrs';
export const ModifyAttrs = 'modify attrs';
export const ModifyProps = 'modify props';
export const ModifyState = 'modify state';
export const ModifyHooks = 'modify hooks';
export const InspectDom = 'inspect component dom';
export const LogComponentData = 'log component data';
export const CopyComponentAttr = 'copy component attr';
// 传递消息来源标志
export const DevToolPanel = 'dev tool panel';
export const DevToolBackground = 'dev tool background';
export const DevToolContentScript = 'dev tool content script';
export const DevToolHook = 'dev tool hook';

View File

@ -1,19 +0,0 @@
function ifNullThrows(v) {
if (v === null) {
throw new Error('received a null');
}
return v;
}
// 用于向页面注入脚本
export function injectCode(src) {
const script = document.createElement('script');
script.src = src;
script.onload = function () {
// 加载完毕后需要移除
script.remove();
};
ifNullThrows(document.head || document.documentElement).appendChild(script);
}

View File

@ -1,19 +0,0 @@
// chrome 通过 iframe 的方式将 panel 页面嵌入到开发者工具中,如果有报错是无法感知到的
// 同时也无法在运行时打断点,需要适当的日志辅助开发和问题定位
interface loggerType {
error: typeof console.error,
info: typeof console.info,
log: typeof console.log,
warn: typeof console.warn,
}
export function createLogger(id: string): loggerType {
return ['error', 'info', 'log', 'warn'].reduce((pre, current) => {
const prefix = `[horizon_dev_tool][${id}] `;
pre[current] = (...data) => {
console[current](prefix, ...data);
};
return pre;
}, {} as loggerType);
}

View File

@ -1,15 +0,0 @@
export function createRegExp(expression: string) {
let str = expression;
if (str[0] === '/') {
str = str.slice(1);
}
if (str[str.length - 1] === '/') {
str = str.slice(0, str.length - 1);
}
try {
return new RegExp(str, 'i');
} catch (err) {
return null;
}
}

View File

@ -1,32 +0,0 @@
const devTools = 'HORIZON_DEV_TOOLS';
interface payLoadType {
type: string,
data?: any,
}
interface message {
type: typeof devTools,
payload: payLoadType,
from: string,
}
export function packagePayload(payload: payLoadType, from: string): message {
return {
type: devTools,
payload,
from,
};
}
export function checkMessage(data: any, from: string) {
if (data?.type === devTools && data?.from === from) {
return true;
}
return false;
}
export function changeSource(message: message, from: string) {
message.from = from;
}

View File

@ -1,19 +0,0 @@
{
"compilerOptions": {
"outDir": "./dist/",
"noImplicitAny": false,
"module": "es6",
"target": "es5",
"jsx": "preserve",
"allowJs": true,
"allowSyntheticDefaultImports": true,
"moduleResolution": "Node",
"baseUrl": "./",
"paths": {
"*": ["types/*"]
}
},
"includes": [
"./src/index.d.ts", "./src/*/*.ts", "./src/*/*.tsx"
]
}

View File

@ -1,73 +0,0 @@
const path = require('path');
const webpack = require('webpack');
const fs = require('fs');
function handleBuildDir() {
const staticDir = path.join(__dirname, 'build');
console.log('staticDir: ', staticDir);
const isBuildExist = fs.existsSync(staticDir);
console.log('isBuildExist: ', isBuildExist);
if (!isBuildExist) {
fs.mkdirSync(staticDir);
}
fs.copyFileSync(path.join(__dirname, 'src', 'panel', 'panel.html'),path.join(staticDir, 'panel.html'));
fs.copyFileSync(path.join(__dirname, 'src', 'main', 'main.html'),path.join(staticDir, 'main.html'));
fs.copyFileSync(path.join(__dirname, 'src', 'manifest.json'),path.join(staticDir, 'manifest.json'));
}
handleBuildDir();
const config = {
entry: {
background: './src/background/index.ts',
main: './src/main/index.ts',
injector: './src/injector/index.ts',
contentScript: './src/contentScript/index.ts',
panel: './src/panel/index.tsx',
},
output: {
path: path.resolve(__dirname, './build'),
filename: '[name].js',
},
mode: 'development',
devtool: 'inline-source-map',
module: {
rules: [
{
test: /\.tsx?$/,
exclude: /node_modules/,
use: [
{
loader: 'babel-loader',
}
]
},
{
test: /\.less/i,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
modules: true,
}
},
'less-loader'],
}]
},
resolve: {
extensions: ['.js', '.ts', '.tsx'],
},
externals: {
'horizon': 'Horizon',
},
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': '"development"',
isDev: 'false',
}),
],
};
module.exports = config;

View File

@ -1,62 +0,0 @@
const path = require('path');
const webpack = require('webpack');
// 用于 panel 页面开发
module.exports = {
mode: 'development',
entry: {
panel: path.join(__dirname, './src/panel/index.tsx'),
mockPage: path.join(__dirname, './src/devtools/mockPage/index.tsx'),
},
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].js'
},
devtool: 'source-map',
resolve: {
extensions: ['.ts', '.tsx', '.js'],
},
module: {
rules: [
{
test: /\.tsx?$/,
exclude: /node_modules/,
use: [
{
loader: 'babel-loader',
}
]
},
{
test: /\.less/i,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
modules: true,
}
},
'less-loader'],
}]
},
externals: {
'horizon': 'Horizon',
},
devServer: {
static: {
directory: path.join(__dirname, 'build'),
},
open: 'panel.html',
port: 9000,
magicHtml: true,
},
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': '"development"',
isDev: 'true',
}),
],
};

View File

@ -1 +0,0 @@
# horizon jsx babel plugin

View File

@ -1,7 +0,0 @@
/* istanbul ignore next */
module.exports = {
presets: [
'@babel/preset-env',
'@babel/preset-typescript'
]
};

View File

@ -1,5 +0,0 @@
module.exports = {
transform: {
'\\.(js|jsx|ts|tsx)$': 'babel-jest',
}
};

View File

@ -1,37 +0,0 @@
{
"name": "@cloudsop/horizon-jsx-transform-babel-plugin",
"version": "0.2.0",
"description": "transform jsx for horizon",
"main": "./dist/index.js",
"scripts": {
"build": "rimraf dist && tsc",
"test": "rimraf dist && tsc && jest",
"jest": "jest",
"prepublish": "npm run build"
},
"files": [
"../dist"
],
"dependencies": {
"@babel/generator": "^7.2.2",
"@babel/parser": "^7.2.3"
},
"devDependencies": {
"@babel/core": "^7.2.2",
"@babel/plugin-proposal-class-properties": "^7.2.1",
"@babel/plugin-proposal-decorators": "^7.2.0",
"@babel/plugin-proposal-export-namespace-from": "^7.2.0",
"@babel/plugin-proposal-function-sent": "^7.2.0",
"@babel/plugin-proposal-json-strings": "^7.2.0",
"@babel/plugin-proposal-numeric-separator": "^7.2.0",
"@babel/plugin-proposal-throw-expressions": "^7.2.0",
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
"@babel/plugin-syntax-import-meta": "^7.2.0",
"@babel/plugin-syntax-jsx": "^7.2.0",
"@babel/plugin-transform-modules-commonjs": "^7.2.0",
"@babel/traverse": "^7.11.0",
"@babel/preset-env": "^7.16.11",
"@babel/types": "^7.0.0",
"babel-plugin-tester": "^10.1.0"
}
}

View File

@ -1,57 +0,0 @@
import SyntaxJSX from '@babel/plugin-syntax-jsx';
import * as BabelCore from '@babel/core';
import * as t from '@babel/types';
import { NodePath } from '@babel/traverse';
import { JSXIdentifier, JSXMemberExpression, JSXNamespacedName } from '@babel/types';
function isHTMLTag(tagName: string) {
return tagName && /^[a-z]/.test(tagName);
}
const horizonJsx = t.memberExpression(t.identifier('Horizon'), t.identifier('jsx'));
function getTagNodeName(tagNode: JSXIdentifier | JSXMemberExpression | JSXNamespacedName) {
let tagName;
if (t.isJSXNamespacedName(tagNode)) {
throw 'horizon jsx doesn\'t support JSX namespace: ' + tagNode;
} else if (t.isJSXIdentifier(tagNode)) {
/*
this -> thisExpression
HTML -> stringLiteral
Others -> Identifier
*/
tagName = tagNode.name === 'this' ?
t.thisExpression() : isHTMLTag(tagNode.name) ?
t.stringLiteral(tagNode.name) :
t.identifier(tagNode.name);
} else if (t.isJSXMemberExpression(tagNode)) {
tagName = t.memberExpression(
getTagNodeName(tagNode.object),
getTagNodeName(tagNode.property),
);
}
return tagName;
}
export default ({ types }: typeof BabelCore) => {
return {
name: 'horizon-jsx-babel-plugin',
inherits: SyntaxJSX,
visitor: {
Program(path: NodePath<t.Program>) {
// program = path
},
JSXElement: {
exit(path: NodePath<t.JSXElement>) {
const openingElement = path.get('openingElement');
const tagName = getTagNodeName(openingElement.node.name);
path.replaceWith(t.callExpression(horizonJsx, [tagName]));
},
},
},
};
};

View File

@ -1,3 +0,0 @@
var x = <div></div>;
var y = <Eview.Table></Eview.Table>;
var z = <this></this>;

View File

@ -1,3 +0,0 @@
var x = Horizon.jsx('div');
var y = Horizon.jsx(Eview.Table);
var z = Horizon.jsx(this);

View File

@ -1 +0,0 @@
<div className="123"></div>

View File

@ -1,10 +0,0 @@
const path = require('path');
const pluginTester = require('babel-plugin-tester').default;
import plugin from '../src';
pluginTester({
plugin,
title: 'horizon jsx plugin',
fixtures: path.join(__dirname, '__fixtures__'),
snapshot: true
});

View File

@ -1,9 +0,0 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist"
},
"include": [
"src"
]
}

View File

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

View File

@ -1,5 +1,6 @@
// 兼容IE的event key // 兼容IE的event key
import { AnyNativeEvent } from './Types';
const uniqueKeyMap = new Map([ const uniqueKeyMap = new Map([
['Esc', 'Escape'], ['Esc', 'Escape'],
['Spacebar', ' '], ['Spacebar', ' '],
@ -10,27 +11,58 @@ const uniqueKeyMap = new Map([
['Del', 'Delete'], ['Del', 'Delete'],
]); ]);
const noop = () => {}; const noop = (): void => {};
// 创建普通自定义事件对象实例,和原生事件对应
export function decorateNativeEvent(customEventName, nativeEvtName, nativeEvent) { // 兼容IE浏览器无法修改Event属性
export class WrappedEvent {
customEventName: string;
nativeEvent: Event;
nativeEventType: string;
type: string;
key: string;
currentTarget: EventTarget | null = null;
stopPropagation: () => void;
preventDefault: () => void;
nativeEvent.isDefaultPrevented = () => nativeEvent.defaultPrevented;
nativeEvent.isPropagationStopped = () => nativeEvent.cancelBubble;
// 适配老版本事件api // 适配老版本事件api
nativeEvent.persist = noop; persist = noop;
constructor(customEventName: string, nativeEvtName: string, nativeEvent: AnyNativeEvent) {
for (const name in nativeEvent) {
this[name] = nativeEvent[name];
}
// stopPropagation和preventDefault 必须通过Event实例调用
this.stopPropagation = () => nativeEvent.stopPropagation();
this.preventDefault = () => nativeEvent.preventDefault();
// custom事件自定义属性 // custom事件自定义属性
nativeEvent.customEventName = customEventName; this.customEventName = customEventName;
nativeEvent.nativeEvent = nativeEvent; this.nativeEvent = nativeEvent;
// 保存原生的事件类型,因为下面会修改 // 保存原生的事件类型,因为下面会修改
nativeEvent.nativeEventType = nativeEvent.type; this.nativeEventType = nativeEvent.type;
Object.defineProperty(nativeEvent, 'type', { writable: true }); this.type = nativeEvtName;
nativeEvent.type = nativeEvtName;
const orgKey = nativeEvent.key; // 兼容IE的event key
Object.defineProperty(nativeEvent, 'key', { writable: true }); const orgKey = (nativeEvent as any).key;
nativeEvent.key = uniqueKeyMap.get(orgKey) || orgKey; this.key = uniqueKeyMap.get(orgKey) || orgKey;
}
return nativeEvent; isDefaultPrevented(): boolean {
return this.nativeEvent.defaultPrevented;
}
isPropagationStopped(): boolean {
return this.nativeEvent.cancelBubble;
}
}
// 创建普通自定义事件对象实例,和原生事件对应
export function decorateNativeEvent(
customEventName: string,
nativeEvtName: string,
nativeEvent: AnyNativeEvent
): WrappedEvent {
return new WrappedEvent(customEventName, nativeEvtName, nativeEvent);
} }

View File

@ -1,6 +1,7 @@
import { VNode } from '../renderer/Types'; import { VNode } from '../renderer/Types';
import { DomComponent } from '../renderer/vnode/VNodeTags'; import { DomComponent } from '../renderer/vnode/VNodeTags';
import { AnyNativeEvent, ListenerUnitList } from './Types'; import { WrappedEvent } from './EventWrapper';
import { ListenerUnitList } from './Types';
import { EVENT_TYPE_ALL, EVENT_TYPE_BUBBLE, EVENT_TYPE_CAPTURE } from './EventHub'; import { EVENT_TYPE_ALL, EVENT_TYPE_BUBBLE, EVENT_TYPE_CAPTURE } from './EventHub';
// 从vnode属性中获取事件listener // 从vnode属性中获取事件listener
@ -25,7 +26,7 @@ function getListenerFromVNode(vNode: VNode, eventName: string): Function | null
export function getListenersFromTree( export function getListenersFromTree(
targetVNode: VNode | null, targetVNode: VNode | null,
horizonEvtName: string | null, horizonEvtName: string | null,
nativeEvent: AnyNativeEvent, nativeEvent: WrappedEvent,
eventType: string eventType: string
): ListenerUnitList { ): ListenerUnitList {
if (!horizonEvtName) { if (!horizonEvtName) {

View File

@ -1,13 +1,17 @@
import type { VNode } from '../renderer/Types';
import type {VNode} from '../renderer/Types'; import { WrappedEvent } from './EventWrapper';
export type AnyNativeEvent = KeyboardEvent | MouseEvent | TouchEvent | UIEvent | Event; export type AnyNativeEvent = KeyboardEvent | MouseEvent | TouchEvent | UIEvent | Event;
export interface HorizonEventListener {
(event: WrappedEvent): void;
}
export type ListenerUnit = { export type ListenerUnit = {
vNode: null | VNode; vNode: null | VNode;
listener: Function; listener: HorizonEventListener;
currentTarget: EventTarget; currentTarget: EventTarget;
event: AnyNativeEvent; event: WrappedEvent;
}; };
export type ListenerUnitList = Array<ListenerUnit>; export type ListenerUnitList = Array<ListenerUnit>;

View File

@ -1,14 +1,38 @@
import { createStore as createStoreX } from '../store/StoreHandler'; import { createStore as createStoreX } from '../store/StoreHandler';
import { ReduxStoreHandler, ReduxAction, ReduxMiddleware } from '../types'; import { ReduxStoreHandler } from '../store/StoreHandler';
export { thunk } from './reduxThunk'; export { thunk } from './reduxThunk';
export { Provider, useSelector, useStore, useDispatch, connect, createSelectorHook, createDispatchHook } from './reduxReact'; export {
Provider,
useSelector,
useStore,
useDispatch,
connect,
createSelectorHook,
createDispatchHook,
} from './reduxReact';
export type ReduxAction = {
type: string;
[key: string]: any;
};
export type ReduxMiddleware = (
store: ReduxStoreHandler,
extraArgument?: any
) => (
next: (action: ReduxAction) => any
) => (
action:
| ReduxAction
| ((dispatch: (action: ReduxAction) => void, store: ReduxStoreHandler, extraArgument?: any) => any)
) => ReduxStoreHandler;
type Reducer = (state: any, action: ReduxAction) => any; type Reducer = (state: any, action: ReduxAction) => any;
export function createStore(reducer: Reducer, preloadedState: any, enhancers): ReduxStoreHandler { export function createStore(reducer: Reducer, preloadedState?: any, enhancers?): ReduxStoreHandler {
const store = createStoreX({ const store = createStoreX({
id: 'defaultStore', id: 'defaultStore',
state: { stateWrapper: preloadedState }, state: { stateWrapper: preloadedState },
@ -35,7 +59,7 @@ export function createStore(reducer: Reducer, preloadedState: any, enhancers): R
const result = { const result = {
reducer, reducer,
getState: function() { getState: function() {
return store.$state.stateWrapper; return store.$s.stateWrapper;
}, },
subscribe: listener => { subscribe: listener => {
store.$subscribe(listener); store.$subscribe(listener);
@ -48,7 +72,7 @@ export function createStore(reducer: Reducer, preloadedState: any, enhancers): R
reducer = newReducer; reducer = newReducer;
}, },
_horizonXstore: store, _horizonXstore: store,
dispatch: store.$actions.dispatch, dispatch: store.$a.dispatch,
}; };
enhancers && enhancers(result); enhancers && enhancers(result);
@ -117,7 +141,6 @@ export function compose(middlewares: ReduxMiddleware[]) {
}; };
} }
// HorizonX batches updates by default, this function is only for backwards compatibility // HorizonX batches updates by default, this function is only for backwards compatibility
export function batch(fn: () => void) { export function batch(fn: () => void) {
fn(); fn();

View File

@ -3,9 +3,10 @@ import { useState, useContext, useEffect, useRef } from '../../renderer/hooks/Ho
import { createContext } from '../../renderer/components/context/CreateContext'; import { createContext } from '../../renderer/components/context/CreateContext';
import { createElement } from '../../external/JSXElement'; import { createElement } from '../../external/JSXElement';
import { BoundActionCreator } from './redux'; import { BoundActionCreator } from './redux';
import { ReduxAction, ReduxStoreHandler } from '../types'; import { ReduxAction } from './redux';
import { ReduxStoreHandler } from '../store/StoreHandler'
const DefaultContext = createContext(); const DefaultContext = createContext(null);
type Context = typeof DefaultContext; type Context = typeof DefaultContext;
export function Provider({ export function Provider({
@ -27,20 +28,15 @@ export function createStoreHook(context: Context) {
}; };
} }
export function createSelectorHook(context: Context): (selector: (any) => any) => any { export function createSelectorHook(context: Context): (selector?: (any) => any) => any {
const store = createStoreHook(context)(); const store = (createStoreHook(context)() as unknown) as ReduxStoreHandler;
return function(selector = state => state) { return function(selector = state => state) {
const [b, fr] = useState(false); const [b, fr] = useState(false);
const listener = () => {
fr(!b);
};
useEffect(() => { useEffect(() => {
const unsubscribe = store.subscribe(listener); const unsubscribe = store.subscribe(() => fr(!b));
return () => { return () => {
unsubscribe(listener); unsubscribe();
}; };
}); });
@ -48,11 +44,11 @@ export function createSelectorHook(context: Context): (selector: (any) => any) =
}; };
} }
export function createDispatchHook(context: Context): BoundActionCreator { export function createDispatchHook(context: Context): ()=>BoundActionCreator {
const store = createStoreHook(context)(); const store = (createStoreHook(context)() as unknown) as ReduxStoreHandler;
return function() { return function() {
return action => { return action => {
this.dispatch(action); store.dispatch(action);
}; };
}.bind(store); }.bind(store);
} }
@ -104,26 +100,32 @@ export function connect(
} }
return Component => { return Component => {
const useStore = createStoreHook(options.context || DefaultContext); const useStore = createStoreHook(options?.context || DefaultContext);
function Wrapper(props) { function Wrapper(props) {
const [f, forceReload] = useState(true); const [f, forceReload] = useState(true);
const store = useStore(); const store = (useStore() as unknown) as ReduxStoreHandler;
useEffect(() => { useEffect(() => {
const unsubscribe = store.subscribe(() => forceReload(!f)); const unsubscribe = store.subscribe(() => forceReload(!f));
() => { return () => {
unsubscribe(() => forceReload(!f)); unsubscribe();
}; };
}); });
const previous = useRef({ const previous = useRef({
state: {}, state: {},
}); mappedState: {},
}) as {
current: {
state: {};
mappedState: {};
};
};
let mappedState; let mappedState;
if (options.areStatesEqual) { if (options?.areStatesEqual) {
if (options.areStatesEqual(previous.current.state, store.getState())) { if (options.areStatesEqual(previous.current.state, store.getState())) {
mappedState = previous.current.mappedState; mappedState = previous.current.mappedState;
} else { } else {

View File

@ -1,4 +1,5 @@
import { ReduxStoreHandler, ReduxAction, ReduxMiddleware } from '../types'; import { ReduxAction, ReduxMiddleware } from './redux';
import { ReduxStoreHandler } from '../store/StoreHandler';
function createThunkMiddleware(extraArgument?: any): ReduxMiddleware { function createThunkMiddleware(extraArgument?: any): ReduxMiddleware {
return (store: ReduxStoreHandler) => (next: (action: ReduxAction) => any) => ( return (store: ReduxStoreHandler) => (next: (action: ReduxAction) => any) => (

View File

@ -1,6 +1,6 @@
// TODO: implement vNode type // TODO: implement vNode type
import {IObserver} from '../types'; import {IObserver} from './Observer';
/** /**
* Observer * Observer

View File

@ -6,7 +6,24 @@
import { launchUpdateFromVNode } from '../../renderer/TreeBuilder'; import { launchUpdateFromVNode } from '../../renderer/TreeBuilder';
import { getProcessingVNode } from '../../renderer/GlobalVar'; import { getProcessingVNode } from '../../renderer/GlobalVar';
import { VNode } from '../../renderer/vnode/VNode'; import { VNode } from '../../renderer/vnode/VNode';
import { IObserver } from '../types'; export interface IObserver {
useProp: (key: string) => void;
addListener: (listener: () => void) => void;
removeListener: (listener: () => void) => void;
setProp: (key: string) => void;
triggerChangeListeners: () => void;
triggerUpdate: (vNode: any) => void;
allChange: () => void;
clearByVNode: (vNode: any) => void;
}
export class Observer implements IObserver { export class Observer implements IObserver {

View File

@ -8,7 +8,9 @@ export function readonlyProxy<T extends object>(target: T): ProxyHandler<T> {
if (isObject(result)) { if (isObject(result)) {
return readonlyProxy(result); return readonlyProxy(result);
} }
} catch {} } catch(err) {
// 不处理
}
return result; return result;
}, },

View File

@ -3,31 +3,84 @@ import { useEffect, useRef } from '../../renderer/hooks/HookExternal';
import { getProcessingVNode } from '../../renderer/GlobalVar'; import { getProcessingVNode } from '../../renderer/GlobalVar';
import { createProxy } from '../proxy/ProxyHandler'; import { createProxy } from '../proxy/ProxyHandler';
import readonlyProxy from '../proxy/readonlyProxy'; import readonlyProxy from '../proxy/readonlyProxy';
import { StoreHandler, StoreConfig, UserActions, UserComputedValues, StoreActions, ComputedValues, ActionFunction, Action, QueuedStoreActions } from '../types';
import { Observer } from '../proxy/Observer'; import { Observer } from '../proxy/Observer';
import { FunctionComponent, ClassComponent } from '../Constants'; import { FunctionComponent, ClassComponent } from '../Constants';
import { VNode } from '../../renderer/Types';
const storeMap = new Map<string,StoreHandler<any,any,any>>(); const storeMap = new Map<string, StoreHandler<any, any, any>>();
function isPromise(obj: any): boolean { function isPromise(obj: any): boolean {
return !!obj && (typeof obj === 'object' || typeof obj === 'function') && typeof obj.then === 'function'; return !!obj && (typeof obj === 'object' || typeof obj === 'function') && typeof obj.then === 'function';
} }
type PlannedAction<S extends object,F extends ActionFunction<S>>={ type StoreConfig<S extends object, A extends UserActions<S>, C extends UserComputedValues<S>> = {
action:string, state?: S;
payload: any[], actions?: A;
resolve: ReturnType<F> id?: string;
} computed?: C;
};
export function createStore<S extends object,A extends UserActions<S>,C extends UserComputedValues<S>>(config: StoreConfig<S,A,C>): () => StoreHandler<S,A,C> { export type ReduxStoreHandler = {
reducer: (state: any, action: { type: string }) => any;
dispatch: (action: { type: string }) => void;
getState: () => any;
subscribe: (listener: () => void) => () => void;
replaceReducer: (reducer: (state: any, action: { type: string }) => any) => void;
};
type StoreHandler<S extends object, A extends UserActions<S>, C extends UserComputedValues<S>> = {
$subscribe: (listener: () => void) => void;
$unsubscribe: (listener: () => void) => void;
$s: S;
$queue: QueuedStoreActions<S, A>;
$a: StoreActions<S, A>;
$c: UserComputedValues<S>;
} & { [K in keyof S]: S[K] } &
{ [K in keyof A]: Action<A[K], S> } &
{ [K in keyof C]: ReturnType<C[K]> };
type PlannedAction<S extends object, F extends ActionFunction<S>> = {
action: string;
payload: any[];
resolve: ReturnType<F>;
};
type RemoveFirstFromTuple<T extends any[]> = T['length'] extends 0
? []
: ((...b: T) => void) extends (a, ...b: infer I) => void
? I
: [];
type UserActions<S extends object> = { [K: string]: ActionFunction<S> };
type UserComputedValues<S extends object> = { [K: string]: ComputedFunction<S> };
type ActionFunction<S extends object> = (this: StoreHandler<S, any, any>, state: S, ...args: any[]) => any;
type ComputedFunction<S extends object> = (state: S) => any;
type Action<T extends ActionFunction<any>, S extends object> = (
this: StoreHandler<S, any, any>,
...args: RemoveFirstFromTuple<Parameters<T>>
) => ReturnType<T>;
type AsyncAction<T extends ActionFunction<any>, S extends object> = (
this: StoreHandler<S, any, any>,
...args: RemoveFirstFromTuple<Parameters<T>>
) => Promise<ReturnType<T>>;
type StoreActions<S extends object, A extends UserActions<S>> = { [K in keyof A]: Action<A[K], S> };
type QueuedStoreActions<S extends object, A extends UserActions<S>> = { [K in keyof A]: AsyncAction<A[K], S> };
type ComputedValues<S extends object, C extends UserComputedValues<S>> = { [K in keyof C]: ReturnType<C[K]> };
type PostponedAction = (state: object, ...args: any[]) => Promise<any>;
type PostponedActions = { [key: string]: PostponedAction };
export function createStore<S extends object, A extends UserActions<S>, C extends UserComputedValues<S>>(
config: StoreConfig<S, A, C>
): () => StoreHandler<S, A, C> {
//create a local shalow copy to ensure consistency (if user would change the config object after store creation) //create a local shalow copy to ensure consistency (if user would change the config object after store creation)
config = { config = {
id:config.id, id: config.id,
options: config.options, options: config.options,
state: config.state, state: config.state,
actions: config.actions ? {...config.actions}:undefined, actions: config.actions ? { ...config.actions } : undefined,
computed: config.computed ? {...config.computed}:undefined computed: config.computed ? { ...config.computed } : undefined,
} };
// 校验 // 校验
if (Object.prototype.toString.call(config) !== '[object Object]') { if (Object.prototype.toString.call(config) !== '[object Object]') {
@ -38,27 +91,27 @@ export function createStore<S extends object,A extends UserActions<S>,C extends
proxyObj.$pending = false; proxyObj.$pending = false;
const $subscribe = (listener) => { const $subscribe = listener => {
proxyObj.addListener(listener); proxyObj.addListener(listener);
}; };
const $unsubscribe = (listener) => { const $unsubscribe = listener => {
proxyObj.removeListener(listener); proxyObj.removeListener(listener);
}; };
const plannedActions:PlannedAction<S,ActionFunction<S>>[] = []; const plannedActions: PlannedAction<S, ActionFunction<S>>[] = [];
const $actions:Partial<StoreActions<S,A>>={} const $a: Partial<StoreActions<S, A>> = {};
const $queue:Partial<StoreActions<S,A>> = {}; const $queue: Partial<StoreActions<S, A>> = {};
const $computed:Partial<ComputedValues<S,C>>={} const $c: Partial<ComputedValues<S, C>> = {};
const handler = { const handler = ({
$subscribe, $subscribe,
$unsubscribe, $unsubscribe,
$actions:$actions as StoreActions<S,A>, $a: $a as StoreActions<S, A>,
$state:proxyObj, $s: proxyObj,
$computed: $computed as ComputedValues<S,C>, $c: $c as ComputedValues<S, C>,
$config:config, $config: config,
$queue: $queue as QueuedStoreActions<S,A>, $queue: $queue as QueuedStoreActions<S, A>,
} as StoreHandler<S,A,C>; } as unknown) as StoreHandler<S, A, C>;
function tryNextAction() { function tryNextAction() {
if (!plannedActions.length) { if (!plannedActions.length) {
@ -67,7 +120,9 @@ export function createStore<S extends object,A extends UserActions<S>,C extends
} }
const nextAction = plannedActions.shift()!; const nextAction = plannedActions.shift()!;
const result = config.actions ? config.actions[nextAction.action].bind(self, proxyObj)(...nextAction.payload) : undefined; const result = config.actions
? config.actions[nextAction.action].bind(handler, proxyObj)(...nextAction.payload)
: undefined;
if (isPromise(result)) { if (isPromise(result)) {
result.then(value => { result.then(value => {
@ -81,16 +136,16 @@ export function createStore<S extends object,A extends UserActions<S>,C extends
} }
// 包装actions // 包装actions
if(config.actions){ if (config.actions) {
Object.keys(config.actions).forEach(action => { Object.keys(config.actions).forEach(action => {
($queue as any)[action] = (...payload) => { ($queue as any)[action] = (...payload) => {
return new Promise((resolve) => { return new Promise(resolve => {
if (!proxyObj.$pending) { if (!proxyObj.$pending) {
proxyObj.$pending = true; proxyObj.$pending = true;
const result = config.actions![action].bind(self, proxyObj)(...payload); const result = config.actions![action].bind(handler, proxyObj)(...payload);
if (isPromise(result)) { if (isPromise(result)) {
result.then((value) => { result.then(value => {
resolve(value); resolve(value);
tryNextAction(); tryNextAction();
}); });
@ -102,40 +157,42 @@ export function createStore<S extends object,A extends UserActions<S>,C extends
plannedActions.push({ plannedActions.push({
action, action,
payload, payload,
resolve resolve,
}); });
} }
}); });
}; };
($actions as any)[action] = function Wrapped(...payload) { ($a as any)[action] = function Wrapped(...payload) {
return config.actions![action].bind(self, proxyObj)(...payload); return config.actions![action].bind(handler, proxyObj)(...payload);
}; };
// direct store access // direct store access
Object.defineProperty(handler, action, { Object.defineProperty(handler, action, {
writable: false, writable: false,
value: $actions[action] value: (...payload) => {
return config.actions![action].bind(handler, proxyObj)(...payload);
},
}); });
}); });
} }
if (config.computed) { if (config.computed) {
Object.keys(config.computed).forEach((key) => { Object.keys(config.computed).forEach(key => {
($computed as any)[key] = config.computed![key].bind(handler, readonlyProxy(proxyObj)); ($c as any)[key] = config.computed![key].bind(handler, readonlyProxy(proxyObj));
// direct store access // direct store access
Object.defineProperty(handler, key, { Object.defineProperty(handler, key, {
get: $computed[key] as ()=>any get: $c[key] as () => any,
}); });
}); });
} }
// direct state access // direct state access
if(config.state){ if (config.state) {
Object.keys(config.state).forEach(key => { Object.keys(config.state).forEach(key => {
Object.defineProperty(handler, key, { Object.defineProperty(handler, key, {
get: () => proxyObj[key] get: () => proxyObj[key],
}); });
}); });
} }
@ -172,7 +229,7 @@ function hookStore() {
if (processingVNode.tag === FunctionComponent) { if (processingVNode.tag === FunctionComponent) {
// from FunctionComponent // from FunctionComponent
const vNodeRef = useRef(null); const vNodeRef = (useRef(null) as unknown) as { current: VNode };
vNodeRef.current = processingVNode; vNodeRef.current = processingVNode;
useEffect(() => { useEffect(() => {
@ -195,13 +252,15 @@ function hookStore() {
function createStoreHook<S extends object, A extends UserActions<S>, C extends UserComputedValues<S>>( function createStoreHook<S extends object, A extends UserActions<S>, C extends UserComputedValues<S>>(
storeHandler: StoreHandler<S, A, C> storeHandler: StoreHandler<S, A, C>
): () => StoreHandler<S, A, C> { ): () => StoreHandler<S, A, C> {
return () => { const storeHook = () => {
if (!storeHandler.$config.options?.suppressHooks) { if (!storeHandler.$config.options?.suppressHooks) {
hookStore(); hookStore();
} }
return storeHandler; return storeHandler;
}; };
return storeHook;
} }
export function useStore<S extends object, A extends UserActions<S>, C extends UserComputedValues<S>>( export function useStore<S extends object, A extends UserActions<S>, C extends UserComputedValues<S>>(
@ -211,9 +270,9 @@ export function useStore<S extends object, A extends UserActions<S>, C extends U
if (storeObj && !storeObj.$config.options?.suppressHooks) hookStore(); if (storeObj && !storeObj.$config.options?.suppressHooks) hookStore();
return storeObj as StoreHandler<S,A,C>; return storeObj as StoreHandler<S, A, C>;
} }
export function clearStore(id:string):void { export function clearStore(id: string): void {
storeMap.delete(id); storeMap.delete(id);
} }

View File

@ -5,11 +5,12 @@
], ],
"scripts": { "scripts": {
"lint": "eslint . --ext .ts", "lint": "eslint . --ext .ts",
"build": " rollup --config ./scripts/rollup/rollup.config.js", "build": "rollup --config ./scripts/rollup/rollup.config.js",
"build:watch": " rollup --watch --config ./scripts/rollup/rollup.config.js", "build:watch": "rollup --watch --config ./scripts/rollup/rollup.config.js",
"build-3rdLib": "node ./scripts/gen3rdLib.js", "build-3rdLib": "node ./scripts/gen3rdLib.js",
"build-3rdLib-dev": "npm run build & node ./scripts/gen3rdLib.js --dev", "build-3rdLib-dev": "npm run build & node ./scripts/gen3rdLib.js --dev",
"build-horizon3rdLib-dev": "npm run build & node ./scripts/gen3rdLib.js --dev --type horizon", "build-horizon3rdLib-dev": "npm run build & node ./scripts/gen3rdLib.js --dev --type horizon",
"build-types": "tsc -p ./libs/horizon/index.ts --emitDeclarationOnly --declaration --declarationDir ./build/horizon/@types --skipLibCheck",
"debug-test": "yarn test --debug", "debug-test": "yarn test --debug",
"test": "jest --config=jest.config.js", "test": "jest --config=jest.config.js",
"watch-test": "yarn test --watch --dev" "watch-test": "yarn test --watch --dev"
@ -29,6 +30,7 @@
"@babel/plugin-transform-destructuring": "7.16.7", "@babel/plugin-transform-destructuring": "7.16.7",
"@babel/plugin-transform-for-of": "7.16.7", "@babel/plugin-transform-for-of": "7.16.7",
"@babel/plugin-transform-literals": "7.16.7", "@babel/plugin-transform-literals": "7.16.7",
"@babel/plugin-transform-object-assign": "7.16.7",
"@babel/plugin-transform-object-super": "7.16.7", "@babel/plugin-transform-object-super": "7.16.7",
"@babel/plugin-transform-parameters": "7.16.7", "@babel/plugin-transform-parameters": "7.16.7",
"@babel/plugin-transform-runtime": "7.16.7", "@babel/plugin-transform-runtime": "7.16.7",
@ -56,6 +58,7 @@
"jest-environment-jsdom-sixteen": "^1.0.3", "jest-environment-jsdom-sixteen": "^1.0.3",
"prettier": "2.6.2", "prettier": "2.6.2",
"rollup": "^2.75.5", "rollup": "^2.75.5",
"rollup-plugin-execute": "^1.1.1",
"rollup-plugin-terser": "^7.0.2", "rollup-plugin-terser": "^7.0.2",
"typescript": "4.2.3" "typescript": "4.2.3"
}, },

View File

@ -1,5 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Dom Textarea should not incur unnecessary DOM mutations 1`] = `<textarea />`;
exports[`Dom Textarea should not incur unnecessary DOM mutations 2`] = `<textarea />`;

View File

@ -1,201 +0,0 @@
import * as Horizon from '@cloudsop/horizon/index.ts';
import { clearStore, createStore, useStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler';
import { App, Text, triggerClickEvent } from '../../jest/commonComponents';
describe('测试store中的Array', () => {
const { unmountComponentAtNode } = Horizon;
let container = null;
beforeEach(() => {
// 创建一个 DOM 元素作为渲染目标
container = document.createElement('div');
document.body.appendChild(container);
const persons = [
{ name: 'p1', age: 1 },
{ name: 'p2', age: 2 },
];
createStore({
id: 'user',
state: {
type: 'bing dun dun',
persons: persons,
},
actions: {
addOnePerson: (state, person) => {
state.persons.push(person);
},
delOnePerson: state => {
state.persons.pop();
},
clearPersons: state => {
state.persons = null;
},
},
});
});
afterEach(() => {
// 退出时进行清理
unmountComponentAtNode(container);
container.remove();
container = null;
clearStore('user');
});
const newPerson = { name: 'p3', age: 3 };
function Parent(props) {
const userStore = useStore('user');
const addOnePerson = function() {
userStore.addOnePerson(newPerson);
};
const delOnePerson = function() {
userStore.delOnePerson();
};
return (
<div>
<button id={'addBtn'} onClick={addOnePerson}>
add person
</button>
<button id={'delBtn'} onClick={delOnePerson}>
delete person
</button>
<div>{props.children}</div>
</div>
);
}
it('测试Array方法: push()、pop()', () => {
function Child(props) {
const userStore = useStore('user');
return (
<div>
<Text id={'hasPerson'} text={`has new person: ${userStore.$state.persons.length}`} />
</div>
);
}
Horizon.render(<App parent={Parent} child={Child} />, container);
expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: 2');
// 在Array中增加一个对象
Horizon.act(() => {
triggerClickEvent(container, 'addBtn');
});
expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: 3');
// 在Array中删除一个对象
Horizon.act(() => {
triggerClickEvent(container, 'delBtn');
});
expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: 2');
});
it('测试Array方法: entries()、push()、shift()、unshift、直接赋值', () => {
let globalStore = null;
function Child(props) {
const userStore = useStore('user');
globalStore = userStore;
const nameList = [];
const entries = userStore.$state.persons?.entries();
if (entries) {
for (const entry of entries) {
nameList.push(entry[1].name);
}
}
return (
<div>
<Text id={'nameList'} text={`name list: ${nameList.join(' ')}`} />
</div>
);
}
Horizon.render(<App parent={Parent} child={Child} />, container);
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2');
// push
globalStore.$state.persons.push(newPerson);
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3');
// shift
globalStore.$state.persons.shift({ name: 'p0', age: 0 });
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p2 p3');
// 赋值[2]
globalStore.$state.persons[2] = { name: 'p4', age: 4 };
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p2 p3 p4');
// 重新赋值[2]
globalStore.$state.persons[2] = { name: 'p5', age: 5 };
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p2 p3 p5');
// unshift
globalStore.$state.persons.unshift({ name: 'p1', age: 1 });
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3 p5');
// 重新赋值 null
globalStore.$state.persons = null;
expect(container.querySelector('#nameList').innerHTML).toBe('name list: ');
// 重新赋值 [{ name: 'p1', age: 1 }]
globalStore.$state.persons = [{ name: 'p1', age: 1 }];
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1');
});
it('测试Array方法: forEach()', () => {
let globalStore = null;
function Child(props) {
const userStore = useStore('user');
globalStore = userStore;
const nameList = [];
userStore.$state.persons?.forEach(per => {
nameList.push(per.name);
});
return (
<div>
<Text id={'nameList'} text={`name list: ${nameList.join(' ')}`} />
</div>
);
}
Horizon.render(<App parent={Parent} child={Child} />, container);
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2');
// push
globalStore.$state.persons.push(newPerson);
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3');
// shift
globalStore.$state.persons.shift({ name: 'p0', age: 0 });
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p2 p3');
// 赋值[2]
globalStore.$state.persons[2] = { name: 'p4', age: 4 };
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p2 p3 p4');
// 重新赋值[2]
globalStore.$state.persons[2] = { name: 'p5', age: 5 };
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p2 p3 p5');
// unshift
globalStore.$state.persons.unshift({ name: 'p1', age: 1 });
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3 p5');
// 重新赋值 null
globalStore.$state.persons = null;
expect(container.querySelector('#nameList').innerHTML).toBe('name list: ');
// 重新赋值 [{ name: 'p1', age: 1 }]
globalStore.$state.persons = [{ name: 'p1', age: 1 }];
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1');
});
});

View File

@ -0,0 +1,208 @@
//@ts-ignore
import * as Horizon from '@cloudsop/horizon/index.ts';
import * as LogUtils from '../../jest/logUtils';
import { clearStore, createStore, useStore } from '../../../../libs/horizon/src/horizonx/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 } = Horizon;
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 (
<div>
<button id={'addBtn'} onClick={addOnePerson}>
add person
</button>
<button id={'delBtn'} onClick={delOnePerson}>
delete person
</button>
<div>{props.children}</div>
</div>
);
}
it('测试Array方法: push()、pop()', () => {
function Child(props) {
const userStore = useUserStore();
return (
<div>
<Text id={'hasPerson'} text={`has new person: ${userStore.$s.persons.length}`} />
</div>
);
}
Horizon.render(<App parent={Parent} child={Child} />, container);
expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: 2');
// 在Array中增加一个对象
Horizon.act(() => {
triggerClickEvent(container, 'addBtn');
});
expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: 3');
// 在Array中删除一个对象
Horizon.act(() => {
triggerClickEvent(container, 'delBtn');
});
expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: 2');
});
it('测试Array方法: entries()、push()、shift()、unshift、直接赋值', () => {
let globalStore = 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 (
<div>
<Text id={'nameList'} text={`name list: ${nameList.join(' ')}`} />
</div>
);
}
Horizon.render(<App parent={Parent} child={Child} />, 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 (
<div>
<Text id={'nameList'} text={`name list: ${nameList.join(' ')}`} />
</div>
);
}
Horizon.render(<App parent={Parent} child={Child} />, 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');
});
});

View File

@ -1,25 +1,18 @@
//@ts-ignore
import * as Horizon from '@cloudsop/horizon/index.ts'; import * as Horizon from '@cloudsop/horizon/index.ts';
import * as LogUtils from '../../jest/logUtils';
import { clearStore, createStore, useStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler'; import { clearStore, createStore, useStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler';
import { App, Text, triggerClickEvent } from '../../jest/commonComponents'; import { App, Text, triggerClickEvent } from '../../jest/commonComponents';
import { describe, beforeEach, afterEach, it, expect } from '@jest/globals';
describe('测试store中的Map', () => { const useUserStore = createStore({
const { unmountComponentAtNode } = Horizon;
let container = null;
beforeEach(() => {
// 创建一个 DOM 元素作为渲染目标
container = document.createElement('div');
document.body.appendChild(container);
const persons = new Map([
['p1', 1],
['p2', 2],
]);
createStore({
id: 'user', id: 'user',
state: { state: {
type: 'bing dun dun', type: 'bing dun dun',
persons: persons, persons: new Map([
['p1', 1],
['p2', 2],
]),
}, },
actions: { actions: {
addOnePerson: (state, person) => { addOnePerson: (state, person) => {
@ -31,15 +24,32 @@ describe('测试store中的Map', () => {
clearPersons: state => { clearPersons: state => {
state.persons.clear(); state.persons.clear();
}, },
reset: state => {
state.persons = new Map([
['p1', 1],
['p2', 2],
]);
}, },
}); },
});
describe('测试store中的Map', () => {
const { unmountComponentAtNode } = Horizon;
let container: HTMLElement | null = null;
beforeEach(() => {
// 创建一个 DOM 元素作为渲染目标
container = document.createElement('div');
document.body.appendChild(container);
useUserStore().reset();
}); });
afterEach(() => { afterEach(() => {
// 退出时进行清理 // 退出时进行清理
unmountComponentAtNode(container); unmountComponentAtNode(container);
container.remove(); container?.remove();
container = null; container = null;
LogUtils.clear();
clearStore('user'); clearStore('user');
}); });
@ -47,7 +57,7 @@ describe('测试store中的Map', () => {
const newPerson = { name: 'p3', age: 3 }; const newPerson = { name: 'p3', age: 3 };
function Parent(props) { function Parent(props) {
const userStore = useStore('user'); const userStore = useUserStore();
const addOnePerson = function() { const addOnePerson = function() {
userStore.addOnePerson(newPerson); userStore.addOnePerson(newPerson);
}; };
@ -76,43 +86,43 @@ describe('测试store中的Map', () => {
it('测试Map方法: set()、delete()、clear()', () => { it('测试Map方法: set()、delete()、clear()', () => {
function Child(props) { function Child(props) {
const userStore = useStore('user'); const userStore = useUserStore();
return ( return (
<div> <div>
<Text id={'size'} text={`persons number: ${userStore.$state.persons.size}`} /> <Text id={'size'} text={`persons number: ${userStore.$s.persons.size}`} />
</div> </div>
); );
} }
Horizon.render(<App parent={Parent} child={Child} />, container); Horizon.render(<App parent={Parent} child={Child} />, container);
expect(container.querySelector('#size').innerHTML).toBe('persons number: 2'); expect(container?.querySelector('#size')?.innerHTML).toBe('persons number: 2');
// 在Map中增加一个对象 // 在Map中增加一个对象
Horizon.act(() => { Horizon.act(() => {
triggerClickEvent(container, 'addBtn'); triggerClickEvent(container, 'addBtn');
}); });
expect(container.querySelector('#size').innerHTML).toBe('persons number: 3'); expect(container?.querySelector('#size')?.innerHTML).toBe('persons number: 3');
// 在Map中删除一个对象 // 在Map中删除一个对象
Horizon.act(() => { Horizon.act(() => {
triggerClickEvent(container, 'delBtn'); triggerClickEvent(container, 'delBtn');
}); });
expect(container.querySelector('#size').innerHTML).toBe('persons number: 2'); expect(container?.querySelector('#size')?.innerHTML).toBe('persons number: 2');
// clear Map // clear Map
Horizon.act(() => { Horizon.act(() => {
triggerClickEvent(container, 'clearBtn'); triggerClickEvent(container, 'clearBtn');
}); });
expect(container.querySelector('#size').innerHTML).toBe('persons number: 0'); expect(container?.querySelector('#size')?.innerHTML).toBe('persons number: 0');
}); });
it('测试Map方法: keys()', () => { it('测试Map方法: keys()', () => {
function Child(props) { function Child(props) {
const userStore = useStore('user'); const userStore = useUserStore();
const nameList = []; const nameList: string[] = [];
const keys = userStore.$state.persons.keys(); const keys = userStore.$s.persons.keys();
for (const key of keys) { for (const key of keys) {
nameList.push(key); nameList.push(key);
} }
@ -126,32 +136,32 @@ describe('测试store中的Map', () => {
Horizon.render(<App parent={Parent} child={Child} />, container); Horizon.render(<App parent={Parent} child={Child} />, container);
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2'); expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2');
// 在Map中增加一个对象 // 在Map中增加一个对象
Horizon.act(() => { Horizon.act(() => {
triggerClickEvent(container, 'addBtn'); triggerClickEvent(container, 'addBtn');
}); });
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3'); expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2 p3');
// 在Map中删除一个对象 // 在Map中删除一个对象
Horizon.act(() => { Horizon.act(() => {
triggerClickEvent(container, 'delBtn'); triggerClickEvent(container, 'delBtn');
}); });
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2'); expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2');
// clear Map // clear Map
Horizon.act(() => { Horizon.act(() => {
triggerClickEvent(container, 'clearBtn'); triggerClickEvent(container, 'clearBtn');
}); });
expect(container.querySelector('#nameList').innerHTML).toBe('name list: '); expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: ');
}); });
it('测试Map方法: values()', () => { it('测试Map方法: values()', () => {
function Child(props) { function Child(props) {
const userStore = useStore('user'); const userStore = useUserStore();
const ageList = []; const ageList: number[] = [];
const values = userStore.$state.persons.values(); const values = userStore.$s.persons.values();
for (const val of values) { for (const val of values) {
ageList.push(val); ageList.push(val);
} }
@ -165,32 +175,32 @@ describe('测试store中的Map', () => {
Horizon.render(<App parent={Parent} child={Child} />, container); Horizon.render(<App parent={Parent} child={Child} />, container);
expect(container.querySelector('#ageList').innerHTML).toBe('age list: 1 2'); expect(container?.querySelector('#ageList')?.innerHTML).toBe('age list: 1 2');
// 在Map中增加一个对象 // 在Map中增加一个对象
Horizon.act(() => { Horizon.act(() => {
triggerClickEvent(container, 'addBtn'); triggerClickEvent(container, 'addBtn');
}); });
expect(container.querySelector('#ageList').innerHTML).toBe('age list: 1 2 3'); expect(container?.querySelector('#ageList')?.innerHTML).toBe('age list: 1 2 3');
// 在Map中删除一个对象 // 在Map中删除一个对象
Horizon.act(() => { Horizon.act(() => {
triggerClickEvent(container, 'delBtn'); triggerClickEvent(container, 'delBtn');
}); });
expect(container.querySelector('#ageList').innerHTML).toBe('age list: 1 2'); expect(container?.querySelector('#ageList')?.innerHTML).toBe('age list: 1 2');
// clear Map // clear Map
Horizon.act(() => { Horizon.act(() => {
triggerClickEvent(container, 'clearBtn'); triggerClickEvent(container, 'clearBtn');
}); });
expect(container.querySelector('#ageList').innerHTML).toBe('age list: '); expect(container?.querySelector('#ageList')?.innerHTML).toBe('age list: ');
}); });
it('测试Map方法: entries()', () => { it('测试Map方法: entries()', () => {
function Child(props) { function Child(props) {
const userStore = useStore('user'); const userStore = useUserStore();
const nameList = []; const nameList: string[] = [];
const entries = userStore.$state.persons.entries(); const entries = userStore.$s.persons.entries();
for (const entry of entries) { for (const entry of entries) {
nameList.push(entry[0]); nameList.push(entry[0]);
} }
@ -204,32 +214,32 @@ describe('测试store中的Map', () => {
Horizon.render(<App parent={Parent} child={Child} />, container); Horizon.render(<App parent={Parent} child={Child} />, container);
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2'); expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2');
// 在Map中增加一个对象 // 在Map中增加一个对象
Horizon.act(() => { Horizon.act(() => {
triggerClickEvent(container, 'addBtn'); triggerClickEvent(container, 'addBtn');
}); });
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3'); expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2 p3');
// 在Map中删除一个对象 // 在Map中删除一个对象
Horizon.act(() => { Horizon.act(() => {
triggerClickEvent(container, 'delBtn'); triggerClickEvent(container, 'delBtn');
}); });
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2'); expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2');
// clear Map // clear Map
Horizon.act(() => { Horizon.act(() => {
triggerClickEvent(container, 'clearBtn'); triggerClickEvent(container, 'clearBtn');
}); });
expect(container.querySelector('#nameList').innerHTML).toBe('name list: '); expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: ');
}); });
it('测试Map方法: forEach()', () => { it('测试Map方法: forEach()', () => {
function Child(props) { function Child(props) {
const userStore = useStore('user'); const userStore = useUserStore();
const nameList = []; const nameList: string[] = [];
userStore.$state.persons.forEach((val, key) => { userStore.$s.persons.forEach((val, key) => {
nameList.push(key); nameList.push(key);
}); });
@ -242,53 +252,53 @@ describe('测试store中的Map', () => {
Horizon.render(<App parent={Parent} child={Child} />, container); Horizon.render(<App parent={Parent} child={Child} />, container);
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2'); expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2');
// 在Map中增加一个对象 // 在Map中增加一个对象
Horizon.act(() => { Horizon.act(() => {
triggerClickEvent(container, 'addBtn'); triggerClickEvent(container, 'addBtn');
}); });
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3'); expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2 p3');
// 在Map中删除一个对象 // 在Map中删除一个对象
Horizon.act(() => { Horizon.act(() => {
triggerClickEvent(container, 'delBtn'); triggerClickEvent(container, 'delBtn');
}); });
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2'); expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2');
// clear Map // clear Map
Horizon.act(() => { Horizon.act(() => {
triggerClickEvent(container, 'clearBtn'); triggerClickEvent(container, 'clearBtn');
}); });
expect(container.querySelector('#nameList').innerHTML).toBe('name list: '); expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: ');
}); });
it('测试Map方法: has()', () => { it('测试Map方法: has()', () => {
function Child(props) { function Child(props) {
const userStore = useStore('user'); const userStore = useUserStore();
return ( return (
<div> <div>
<Text id={'hasPerson'} text={`has new person: ${userStore.$state.persons.has(newPerson.name)}`} /> <Text id={'hasPerson'} text={`has new person: ${userStore.$s.persons.has(newPerson.name)}`} />
</div> </div>
); );
} }
Horizon.render(<App parent={Parent} child={Child} />, container); Horizon.render(<App parent={Parent} child={Child} />, container);
expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: false'); expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: false');
// 在Map中增加一个对象 // 在Map中增加一个对象
Horizon.act(() => { Horizon.act(() => {
triggerClickEvent(container, 'addBtn'); triggerClickEvent(container, 'addBtn');
}); });
expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: true'); expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: true');
}); });
it('测试Map方法: for of()', () => { it('测试Map方法: for of()', () => {
function Child(props) { function Child(props) {
const userStore = useStore('user'); const userStore = useUserStore();
const nameList = []; const nameList: string[] = [];
for (const per of userStore.$state.persons) { for (const per of userStore.$s.persons) {
nameList.push(per[0]); nameList.push(per[0]);
} }
@ -301,23 +311,23 @@ describe('测试store中的Map', () => {
Horizon.render(<App parent={Parent} child={Child} />, container); Horizon.render(<App parent={Parent} child={Child} />, container);
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2'); expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2');
// 在Map中增加一个对象 // 在Map中增加一个对象
Horizon.act(() => { Horizon.act(() => {
triggerClickEvent(container, 'addBtn'); triggerClickEvent(container, 'addBtn');
}); });
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3'); expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2 p3');
// 在Map中删除一个对象 // 在Map中删除一个对象
Horizon.act(() => { Horizon.act(() => {
triggerClickEvent(container, 'delBtn'); triggerClickEvent(container, 'delBtn');
}); });
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2'); expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2');
// clear Map // clear Map
Horizon.act(() => { Horizon.act(() => {
triggerClickEvent(container, 'clearBtn'); triggerClickEvent(container, 'clearBtn');
}); });
expect(container.querySelector('#nameList').innerHTML).toBe('name list: '); expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: ');
}); });
}); });

View File

@ -1,10 +1,12 @@
//@ts-ignore
import * as Horizon from '@cloudsop/horizon/index.ts'; import * as Horizon from '@cloudsop/horizon/index.ts';
import * as LogUtils from '../../jest/logUtils';
import { clearStore, createStore, useStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler'; import { clearStore, createStore, useStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler';
import { App, Text, triggerClickEvent } from '../../jest/commonComponents'; import { App, Text, triggerClickEvent } from '../../jest/commonComponents';
describe('测试store中的混合类型变化', () => { describe('测试store中的混合类型变化', () => {
const { unmountComponentAtNode } = Horizon; const { unmountComponentAtNode } = Horizon;
let container = null; let container: HTMLElement | null = null;
beforeEach(() => { beforeEach(() => {
// 创建一个 DOM 元素作为渲染目标 // 创建一个 DOM 元素作为渲染目标
container = document.createElement('div'); container = document.createElement('div');
@ -42,8 +44,9 @@ describe('测试store中的混合类型变化', () => {
afterEach(() => { afterEach(() => {
// 退出时进行清理 // 退出时进行清理
unmountComponentAtNode(container); unmountComponentAtNode(container);
container.remove(); (container as HTMLElement).remove();
container = null; container = null;
LogUtils.clear();
clearStore('user'); clearStore('user');
}); });
@ -68,7 +71,7 @@ describe('测试store中的混合类型变化', () => {
function Child(props) { function Child(props) {
const userStore = useStore('user'); const userStore = useStore('user');
const days = userStore.$state.persons const days = userStore.persons
.values() .values()
.next() .next()
.value.love.get('lanqiu').days; .value.love.get('lanqiu').days;
@ -82,11 +85,11 @@ describe('测试store中的混合类型变化', () => {
Horizon.render(<App parent={Parent} child={Child} />, container); Horizon.render(<App parent={Parent} child={Child} />, container);
expect(container.querySelector('#dayList').innerHTML).toBe('love: 1 3 5'); expect(container?.querySelector('#dayList')?.innerHTML).toBe('love: 1 3 5');
Horizon.act(() => { Horizon.act(() => {
triggerClickEvent(container, 'addBtn'); triggerClickEvent(container, 'addBtn');
}); });
expect(container.querySelector('#dayList').innerHTML).toBe('love: 1 3 5 7'); expect(container?.querySelector('#dayList')?.innerHTML).toBe('love: 1 3 5 7');
}); });
it('属性是个class实例', () => { it('属性是个class实例', () => {
@ -103,7 +106,6 @@ describe('测试store中的混合类型变化', () => {
setName(name) { setName(name) {
this.name = name; this.name = name;
} }
getName() { getName() {
return this.name; return this.name;
} }
@ -111,7 +113,6 @@ describe('测试store中的混合类型变化', () => {
setAge(age) { setAge(age) {
this.age = age; this.age = age;
} }
getAge() { getAge() {
return this.age; return this.age;
} }
@ -119,7 +120,6 @@ describe('测试store中的混合类型变化', () => {
addLove(lv) { addLove(lv) {
this.loves.add(lv); this.loves.add(lv);
} }
getLoves() { getLoves() {
return this.loves; return this.loves;
} }
@ -127,14 +127,19 @@ describe('测试store中的混合类型变化', () => {
let globalPerson; let globalPerson;
let globalStore; let globalStore;
function Child(props) { function Child(props) {
const userStore = useStore('user'); const userStore = useStore('user');
globalStore = userStore; globalStore = userStore;
const nameList = []; const nameList: string[] = [];
const valIterator = userStore.$state.persons.values(); const valIterator = userStore.persons.values();
let per = valIterator.next(); let per = valIterator.next() as {
value: {
name: string;
getName: () => string;
};
done: boolean;
};
while (!per.done) { while (!per.done) {
nameList.push(per.value.name ?? per.value.getName()); nameList.push(per.value.name ?? per.value.getName());
globalPerson = per.value; globalPerson = per.value;
@ -150,15 +155,15 @@ describe('测试store中的混合类型变化', () => {
Horizon.render(<App parent={Parent} child={Child} />, container); Horizon.render(<App parent={Parent} child={Child} />, container);
expect(container.querySelector('#nameList').innerHTML).toBe('p1 p2'); expect(container?.querySelector('#nameList')?.innerHTML).toBe('p1 p2');
// 动态增加一个Person实例 // 动态增加一个Person实例
globalStore.$state.persons.add(new Person('ClassPerson', 5)); globalStore.$s.persons.add(new Person('ClassPerson', 5));
expect(container.querySelector('#nameList').innerHTML).toBe('p1 p2 ClassPerson'); expect(container?.querySelector('#nameList')?.innerHTML).toBe('p1 p2 ClassPerson');
globalPerson.setName('ClassPerson1'); globalPerson.setName('ClassPerson1');
expect(container.querySelector('#nameList').innerHTML).toBe('p1 p2 ClassPerson1'); expect(container?.querySelector('#nameList')?.innerHTML).toBe('p1 p2 ClassPerson1');
}); });
}); });

View File

@ -1,25 +1,18 @@
//@ts-ignore
import * as Horizon from '@cloudsop/horizon/index.ts'; import * as Horizon from '@cloudsop/horizon/index.ts';
import * as LogUtils from '../../jest/logUtils';
import { clearStore, createStore, useStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler'; import { clearStore, createStore, useStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler';
import { App, Text, triggerClickEvent } from '../../jest/commonComponents'; import { App, Text, triggerClickEvent } from '../../jest/commonComponents';
import { describe, beforeEach, afterEach, it, expect } from '@jest/globals';
describe('测试store中的Set', () => { const useUserStore = createStore({
const { unmountComponentAtNode } = Horizon;
let container = null;
beforeEach(() => {
// 创建一个 DOM 元素作为渲染目标
container = document.createElement('div');
document.body.appendChild(container);
const persons = new Set([
{ name: 'p1', age: 1 },
{ name: 'p2', age: 2 },
]);
createStore({
id: 'user', id: 'user',
state: { state: {
type: 'bing dun dun', type: 'bing dun dun',
persons: persons, persons: new Set([
{ name: 'p1', age: 1 },
{ name: 'p2', age: 2 },
]),
}, },
actions: { actions: {
addOnePerson: (state, person) => { addOnePerson: (state, person) => {
@ -31,23 +24,39 @@ describe('测试store中的Set', () => {
clearPersons: state => { clearPersons: state => {
state.persons.clear(); state.persons.clear();
}, },
reset: state => {
state.persons = new Set([
{ name: 'p1', age: 1 },
{ name: 'p2', age: 2 },
]);
}, },
}); },
});
describe('测试store中的Set', () => {
const { unmountComponentAtNode } = Horizon;
let container: HTMLElement | null = null;
beforeEach(() => {
// 创建一个 DOM 元素作为渲染目标
container = document.createElement('div');
document.body.appendChild(container);
useUserStore().reset();
}); });
afterEach(() => { afterEach(() => {
// 退出时进行清理 // 退出时进行清理
unmountComponentAtNode(container); unmountComponentAtNode(container);
container.remove(); container?.remove();
container = null; container = null;
LogUtils.clear();
clearStore('user'); clearStore('user');
}); });
const newPerson = { name: 'p3', age: 3 }; const newPerson = { name: 'p3', age: 3 };
function Parent(props) { function Parent(props) {
const userStore = useStore('user'); const userStore = useUserStore();
const addOnePerson = function() { const addOnePerson = function() {
userStore.addOnePerson(newPerson); userStore.addOnePerson(newPerson);
}; };
@ -76,17 +85,17 @@ describe('测试store中的Set', () => {
it('测试Set方法: add()、delete()、clear()', () => { it('测试Set方法: add()、delete()、clear()', () => {
function Child(props) { function Child(props) {
const userStore = useStore('user'); const userStore = useUserStore();
const personArr = Array.from(userStore.$state.persons); const personArr = Array.from(userStore.$s.persons);
const nameList = []; const nameList: string[] = [];
const keys = userStore.$state.persons.keys(); const keys = userStore.$s.persons.keys();
for (const key of keys) { for (const key of keys) {
nameList.push(key.name); nameList.push(key.name);
} }
return ( return (
<div> <div>
<Text id={'size'} text={`persons number: ${userStore.$state.persons.size}`} /> <Text id={'size'} text={`persons number: ${userStore.$s.persons.size}`} />
<Text id={'lastAge'} text={`last person age: ${personArr[personArr.length - 1]?.age ?? 0}`} /> <Text id={'lastAge'} text={`last person age: ${personArr[personArr.length - 1]?.age ?? 0}`} />
</div> </div>
); );
@ -94,35 +103,35 @@ describe('测试store中的Set', () => {
Horizon.render(<App parent={Parent} child={Child} />, container); Horizon.render(<App parent={Parent} child={Child} />, container);
expect(container.querySelector('#size').innerHTML).toBe('persons number: 2'); expect(container?.querySelector('#size')?.innerHTML).toBe('persons number: 2');
expect(container.querySelector('#lastAge').innerHTML).toBe('last person age: 2'); expect(container?.querySelector('#lastAge')?.innerHTML).toBe('last person age: 2');
// 在set中增加一个对象 // 在set中增加一个对象
Horizon.act(() => { Horizon.act(() => {
triggerClickEvent(container, 'addBtn'); triggerClickEvent(container, 'addBtn');
}); });
expect(container.querySelector('#size').innerHTML).toBe('persons number: 3'); expect(container?.querySelector('#size')?.innerHTML).toBe('persons number: 3');
// 在set中删除一个对象 // 在set中删除一个对象
Horizon.act(() => { Horizon.act(() => {
triggerClickEvent(container, 'delBtn'); triggerClickEvent(container, 'delBtn');
}); });
expect(container.querySelector('#size').innerHTML).toBe('persons number: 2'); expect(container?.querySelector('#size')?.innerHTML).toBe('persons number: 2');
// clear set // clear set
Horizon.act(() => { Horizon.act(() => {
triggerClickEvent(container, 'clearBtn'); triggerClickEvent(container, 'clearBtn');
}); });
expect(container.querySelector('#size').innerHTML).toBe('persons number: 0'); expect(container?.querySelector('#size')?.innerHTML).toBe('persons number: 0');
expect(container.querySelector('#lastAge').innerHTML).toBe('last person age: 0'); expect(container?.querySelector('#lastAge')?.innerHTML).toBe('last person age: 0');
}); });
it('测试Set方法: keys()、values()', () => { it('测试Set方法: keys()、values()', () => {
function Child(props) { function Child(props) {
const userStore = useStore('user'); const userStore = useUserStore();
const nameList = []; const nameList: string[] = [];
const keys = userStore.$state.persons.keys(); const keys = userStore.$s.persons.keys();
// const keys = userStore.$state.persons.values(); // const keys = userStore.$s.persons.values();
for (const key of keys) { for (const key of keys) {
nameList.push(key.name); nameList.push(key.name);
} }
@ -136,32 +145,32 @@ describe('测试store中的Set', () => {
Horizon.render(<App parent={Parent} child={Child} />, container); Horizon.render(<App parent={Parent} child={Child} />, container);
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2'); expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2');
// 在set中增加一个对象 // 在set中增加一个对象
Horizon.act(() => { Horizon.act(() => {
triggerClickEvent(container, 'addBtn'); triggerClickEvent(container, 'addBtn');
}); });
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3'); expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2 p3');
// 在set中删除一个对象 // 在set中删除一个对象
Horizon.act(() => { Horizon.act(() => {
triggerClickEvent(container, 'delBtn'); triggerClickEvent(container, 'delBtn');
}); });
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2'); expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2');
// clear set // clear set
Horizon.act(() => { Horizon.act(() => {
triggerClickEvent(container, 'clearBtn'); triggerClickEvent(container, 'clearBtn');
}); });
expect(container.querySelector('#nameList').innerHTML).toBe('name list: '); expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: ');
}); });
it('测试Set方法: entries()', () => { it('测试Set方法: entries()', () => {
function Child(props) { function Child(props) {
const userStore = useStore('user'); const userStore = useUserStore();
const nameList = []; const nameList: string[] = [];
const entries = userStore.$state.persons.entries(); const entries = userStore.$s.persons.entries();
for (const entry of entries) { for (const entry of entries) {
nameList.push(entry[0].name); nameList.push(entry[0].name);
} }
@ -175,32 +184,32 @@ describe('测试store中的Set', () => {
Horizon.render(<App parent={Parent} child={Child} />, container); Horizon.render(<App parent={Parent} child={Child} />, container);
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2'); expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2');
// 在set中增加一个对象 // 在set中增加一个对象
Horizon.act(() => { Horizon.act(() => {
triggerClickEvent(container, 'addBtn'); triggerClickEvent(container, 'addBtn');
}); });
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3'); expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2 p3');
// 在set中删除一个对象 // 在set中删除一个对象
Horizon.act(() => { Horizon.act(() => {
triggerClickEvent(container, 'delBtn'); triggerClickEvent(container, 'delBtn');
}); });
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2'); expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2');
// clear set // clear set
Horizon.act(() => { Horizon.act(() => {
triggerClickEvent(container, 'clearBtn'); triggerClickEvent(container, 'clearBtn');
}); });
expect(container.querySelector('#nameList').innerHTML).toBe('name list: '); expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: ');
}); });
it('测试Set方法: forEach()', () => { it('测试Set方法: forEach()', () => {
function Child(props) { function Child(props) {
const userStore = useStore('user'); const userStore = useUserStore();
const nameList = []; const nameList: string[] = [];
userStore.$state.persons.forEach(per => { userStore.$s.persons.forEach(per => {
nameList.push(per.name); nameList.push(per.name);
}); });
@ -213,53 +222,53 @@ describe('测试store中的Set', () => {
Horizon.render(<App parent={Parent} child={Child} />, container); Horizon.render(<App parent={Parent} child={Child} />, container);
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2'); expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2');
// 在set中增加一个对象 // 在set中增加一个对象
Horizon.act(() => { Horizon.act(() => {
triggerClickEvent(container, 'addBtn'); triggerClickEvent(container, 'addBtn');
}); });
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3'); expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2 p3');
// 在set中删除一个对象 // 在set中删除一个对象
Horizon.act(() => { Horizon.act(() => {
triggerClickEvent(container, 'delBtn'); triggerClickEvent(container, 'delBtn');
}); });
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2'); expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2');
// clear set // clear set
Horizon.act(() => { Horizon.act(() => {
triggerClickEvent(container, 'clearBtn'); triggerClickEvent(container, 'clearBtn');
}); });
expect(container.querySelector('#nameList').innerHTML).toBe('name list: '); expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: ');
}); });
it('测试Set方法: has()', () => { it('测试Set方法: has()', () => {
function Child(props) { function Child(props) {
const userStore = useStore('user'); const userStore = useUserStore();
return ( return (
<div> <div>
<Text id={'hasPerson'} text={`has new person: ${userStore.$state.persons.has(newPerson)}`} /> <Text id={'hasPerson'} text={`has new person: ${userStore.$s.persons.has(newPerson)}`} />
</div> </div>
); );
} }
Horizon.render(<App parent={Parent} child={Child} />, container); Horizon.render(<App parent={Parent} child={Child} />, container);
expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: false'); expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: false');
// 在set中增加一个对象 // 在set中增加一个对象
Horizon.act(() => { Horizon.act(() => {
triggerClickEvent(container, 'addBtn'); triggerClickEvent(container, 'addBtn');
}); });
expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: true'); expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: true');
}); });
it('测试Set方法: for of()', () => { it('测试Set方法: for of()', () => {
function Child(props) { function Child(props) {
const userStore = useStore('user'); const userStore = useUserStore();
const nameList = []; const nameList: string[] = [];
for (const per of userStore.$state.persons) { for (const per of userStore.$s.persons) {
nameList.push(per.name); nameList.push(per.name);
} }
@ -272,23 +281,23 @@ describe('测试store中的Set', () => {
Horizon.render(<App parent={Parent} child={Child} />, container); Horizon.render(<App parent={Parent} child={Child} />, container);
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2'); expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2');
// 在set中增加一个对象 // 在set中增加一个对象
Horizon.act(() => { Horizon.act(() => {
triggerClickEvent(container, 'addBtn'); triggerClickEvent(container, 'addBtn');
}); });
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3'); expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2 p3');
// 在set中删除一个对象 // 在set中删除一个对象
Horizon.act(() => { Horizon.act(() => {
triggerClickEvent(container, 'delBtn'); triggerClickEvent(container, 'delBtn');
}); });
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2'); expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2');
// clear set // clear set
Horizon.act(() => { Horizon.act(() => {
triggerClickEvent(container, 'clearBtn'); triggerClickEvent(container, 'clearBtn');
}); });
expect(container.querySelector('#nameList').innerHTML).toBe('name list: '); expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: ');
}); });
}); });

View File

@ -1,25 +1,18 @@
//@ts-ignore
import * as Horizon from '@cloudsop/horizon/index.ts'; import * as Horizon from '@cloudsop/horizon/index.ts';
import * as LogUtils from '../../jest/logUtils';
import { clearStore, createStore, useStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler'; import { clearStore, createStore, useStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler';
import { App, Text, triggerClickEvent } from '../../jest/commonComponents'; import { App, Text, triggerClickEvent } from '../../jest/commonComponents';
import { describe, beforeEach, afterEach, it, expect } from '@jest/globals';
describe('测试store中的WeakMap', () => { const useUserStore = createStore({
const { unmountComponentAtNode } = Horizon;
let container = null;
beforeEach(() => {
// 创建一个 DOM 元素作为渲染目标
container = document.createElement('div');
document.body.appendChild(container);
const persons = new WeakMap([
[{ name: 'p1' }, 1],
[{ name: 'p2' }, 2],
]);
createStore({
id: 'user', id: 'user',
state: { state: {
type: 'bing dun dun', type: 'bing dun dun',
persons: persons, persons: new WeakMap([
[{ name: 'p1' }, 1],
[{ name: 'p2' }, 2],
]),
}, },
actions: { actions: {
addOnePerson: (state, person) => { addOnePerson: (state, person) => {
@ -29,17 +22,34 @@ describe('测试store中的WeakMap', () => {
state.persons.delete(person); state.persons.delete(person);
}, },
clearPersons: state => { clearPersons: state => {
state.persons.clear(); state.persons = new WeakMap([]);
},
reset: state => {
state.persons = new WeakMap([
[{ name: 'p1' }, 1],
[{ name: 'p2' }, 2],
]);
}, },
}, },
}); });
describe('测试store中的WeakMap', () => {
const { unmountComponentAtNode } = Horizon;
let container: HTMLElement | null = null;
beforeEach(() => {
// 创建一个 DOM 元素作为渲染目标
container = document.createElement('div');
document.body.appendChild(container);
useUserStore().reset();
}); });
afterEach(() => { afterEach(() => {
// 退出时进行清理 // 退出时进行清理
unmountComponentAtNode(container); unmountComponentAtNode(container);
container.remove(); container?.remove();
container = null; container = null;
LogUtils.clear();
clearStore('user'); clearStore('user');
}); });
@ -47,7 +57,7 @@ describe('测试store中的WeakMap', () => {
const newPerson = { name: 'p3' }; const newPerson = { name: 'p3' };
function Parent(props) { function Parent(props) {
const userStore = useStore('user'); const userStore = useUserStore();
const addOnePerson = function() { const addOnePerson = function() {
userStore.addOnePerson(newPerson); userStore.addOnePerson(newPerson);
}; };
@ -76,49 +86,49 @@ describe('测试store中的WeakMap', () => {
it('测试WeakMap方法: set()、delete()、has()', () => { it('测试WeakMap方法: set()、delete()、has()', () => {
function Child(props) { function Child(props) {
const userStore = useStore('user'); const userStore = useUserStore();
return ( return (
<div> <div>
<Text id={'hasPerson'} text={`has new person: ${userStore.$state.persons.has(newPerson)}`} /> <Text id={'hasPerson'} text={`has new person: ${userStore.$s.persons.has(newPerson)}`} />
</div> </div>
); );
} }
Horizon.render(<App parent={Parent} child={Child} />, container); Horizon.render(<App parent={Parent} child={Child} />, container);
expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: false'); expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: false');
// 在WeakMap中增加一个对象 // 在WeakMap中增加一个对象
Horizon.act(() => { Horizon.act(() => {
triggerClickEvent(container, 'addBtn'); triggerClickEvent(container, 'addBtn');
}); });
expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: true'); expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: true');
// 在WeakMap中删除一个对象 // 在WeakMap中删除一个对象
Horizon.act(() => { Horizon.act(() => {
triggerClickEvent(container, 'delBtn'); triggerClickEvent(container, 'delBtn');
}); });
expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: false'); expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: false');
}); });
it('测试WeakMap方法: get()', () => { it('测试WeakMap方法: get()', () => {
function Child(props) { function Child(props) {
const userStore = useStore('user'); const userStore = useUserStore();
return ( return (
<div> <div>
<Text id={'hasPerson'} text={`has new person: ${userStore.$state.persons.get(newPerson)}`} /> <Text id={'hasPerson'} text={`has new person: ${userStore.$s.persons.get(newPerson)}`} />
</div> </div>
); );
} }
Horizon.render(<App parent={Parent} child={Child} />, container); Horizon.render(<App parent={Parent} child={Child} />, container);
expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: undefined'); expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: undefined');
// 在WeakMap中增加一个对象 // 在WeakMap中增加一个对象
Horizon.act(() => { Horizon.act(() => {
triggerClickEvent(container, 'addBtn'); triggerClickEvent(container, 'addBtn');
}); });
expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: 3'); expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: 3');
}); });
}); });

View File

@ -1,25 +1,18 @@
//@ts-ignore
import * as Horizon from '@cloudsop/horizon/index.ts'; import * as Horizon from '@cloudsop/horizon/index.ts';
import * as LogUtils from '../../jest/logUtils';
import { clearStore, createStore, useStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler'; import { clearStore, createStore, useStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler';
import { App, Text, triggerClickEvent } from '../../jest/commonComponents'; import { App, Text, triggerClickEvent } from '../../jest/commonComponents';
import { describe, beforeEach, afterEach, it, expect } from '@jest/globals';
describe('测试store中的WeakSet', () => { const useUserStore = createStore({
const { unmountComponentAtNode } = Horizon;
let container = null;
beforeEach(() => {
// 创建一个 DOM 元素作为渲染目标
container = document.createElement('div');
document.body.appendChild(container);
const persons = new WeakSet([
{ name: 'p1', age: 1 },
{ name: 'p2', age: 2 },
]);
createStore({
id: 'user', id: 'user',
state: { state: {
type: 'bing dun dun', type: 'bing dun dun',
persons: persons, persons: new WeakSet([
{ name: 'p1', age: 1 },
{ name: 'p2', age: 2 },
]),
}, },
actions: { actions: {
addOnePerson: (state, person) => { addOnePerson: (state, person) => {
@ -29,25 +22,41 @@ describe('测试store中的WeakSet', () => {
state.persons.delete(person); state.persons.delete(person);
}, },
clearPersons: state => { clearPersons: state => {
state.persons.clear(); state.persons = new WeakSet([]);
},
reset: state => {
state.persons = new WeakSet([
{ name: 'p1', age: 1 },
{ name: 'p2', age: 2 },
]);
}, },
}, },
}); });
describe('测试store中的WeakSet', () => {
const { unmountComponentAtNode } = Horizon;
let container: HTMLElement | null = null;
beforeEach(() => {
// 创建一个 DOM 元素作为渲染目标
container = document.createElement('div');
document.body.appendChild(container);
useUserStore().reset();
}); });
afterEach(() => { afterEach(() => {
// 退出时进行清理 // 退出时进行清理
unmountComponentAtNode(container); unmountComponentAtNode(container);
container.remove(); container?.remove();
container = null; container = null;
LogUtils.clear();
clearStore('user'); clearStore('user');
}); });
const newPerson = { name: 'p3', age: 3 }; const newPerson = { name: 'p3', age: 3 };
function Parent(props) { function Parent(props) {
const userStore = useStore('user'); const userStore = useUserStore();
const addOnePerson = function() { const addOnePerson = function() {
userStore.addOnePerson(newPerson); userStore.addOnePerson(newPerson);
}; };
@ -69,28 +78,28 @@ describe('测试store中的WeakSet', () => {
it('测试WeakSet方法: add()、delete()、has()', () => { it('测试WeakSet方法: add()、delete()、has()', () => {
function Child(props) { function Child(props) {
const userStore = useStore('user'); const userStore = useUserStore();
return ( return (
<div> <div>
<Text id={'hasPerson'} text={`has new person: ${userStore.$state.persons.has(newPerson)}`} /> <Text id={'hasPerson'} text={`has new person: ${userStore.$s.persons.has(newPerson)}`} />
</div> </div>
); );
} }
Horizon.render(<App parent={Parent} child={Child} />, container); Horizon.render(<App parent={Parent} child={Child} />, container);
expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: false'); expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: false');
// 在WeakSet中增加一个对象 // 在WeakSet中增加一个对象
Horizon.act(() => { Horizon.act(() => {
triggerClickEvent(container, 'addBtn'); triggerClickEvent(container, 'addBtn');
}); });
expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: true'); expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: true');
// 在WeakSet中删除一个对象 // 在WeakSet中删除一个对象
Horizon.act(() => { Horizon.act(() => {
triggerClickEvent(container, 'delBtn'); triggerClickEvent(container, 'delBtn');
}); });
expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: false'); expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: false');
}); });
}); });

View File

@ -1,6 +1,8 @@
//@ts-ignore
import * as Horizon from '@cloudsop/horizon/index.ts'; import * as Horizon from '@cloudsop/horizon/index.ts';
import { createStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler'; import { createStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler';
import { triggerClickEvent } from '../../jest/commonComponents'; import { triggerClickEvent } from '../../jest/commonComponents';
import { describe, beforeEach, afterEach, it, expect } from '@jest/globals';
const { unmountComponentAtNode } = Horizon; const { unmountComponentAtNode } = Horizon;
@ -13,7 +15,7 @@ function postpone(timer, func) {
} }
describe('Asynchronous functions', () => { describe('Asynchronous functions', () => {
let container = null; let container: HTMLElement | null = null;
const COUNTER_ID = 'counter'; const COUNTER_ID = 'counter';
const TOGGLE_ID = 'toggle'; const TOGGLE_ID = 'toggle';
@ -33,7 +35,7 @@ describe('Asynchronous functions', () => {
return new Promise(resolve => { return new Promise(resolve => {
setTimeout(() => { setTimeout(() => {
state.counter++; state.counter++;
resolve(); resolve(true);
}, 100); }, 100);
}); });
}, },
@ -53,11 +55,12 @@ describe('Asynchronous functions', () => {
afterEach(() => { afterEach(() => {
unmountComponentAtNode(container); unmountComponentAtNode(container);
container.remove(); container?.remove();
container = null; container = null;
}); });
it('Should wait for async actions', async () => { it('Should wait for async actions', async () => {
// @ts-ignore
jest.useRealTimers(); jest.useRealTimers();
let globalStore; let globalStore;
@ -84,14 +87,14 @@ describe('Asynchronous functions', () => {
Horizon.render(<App />, container); Horizon.render(<App />, container);
// initial state // initial state
expect(document.getElementById(RESULT_ID).innerHTML).toBe('false0'); expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('false0');
// slow toggle has nothing to wait for, it is resolved immediately // slow toggle has nothing to wait for, it is resolved immediately
Horizon.act(() => { Horizon.act(() => {
triggerClickEvent(container, TOGGLE_ID); triggerClickEvent(container, TOGGLE_ID);
}); });
expect(document.getElementById(RESULT_ID).innerHTML).toBe('true0'); expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('true0');
// counter increment is slow. slow toggle waits for result // counter increment is slow. slow toggle waits for result
Horizon.act(() => { Horizon.act(() => {
@ -101,18 +104,18 @@ describe('Asynchronous functions', () => {
triggerClickEvent(container, TOGGLE_ID); triggerClickEvent(container, TOGGLE_ID);
}); });
expect(document.getElementById(RESULT_ID).innerHTML).toBe('true0'); expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('true0');
// fast toggle does not wait for counter and it is resolved immediately // fast toggle does not wait for counter and it is resolved immediately
Horizon.act(() => { Horizon.act(() => {
triggerClickEvent(container, TOGGLE_FAST_ID); triggerClickEvent(container, TOGGLE_FAST_ID);
}); });
expect(document.getElementById(RESULT_ID).innerHTML).toBe('false0'); expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('false0');
// at 150ms counter increment will be resolved and slow toggle immediately after // at 150ms counter increment will be resolved and slow toggle immediately after
const t150 = postpone(150, () => { const t150 = postpone(150, () => {
expect(document.getElementById(RESULT_ID).innerHTML).toBe('true1'); expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('true1');
}); });
// before that, two more actions are added to queue - another counter and slow toggle // before that, two more actions are added to queue - another counter and slow toggle
@ -125,13 +128,14 @@ describe('Asynchronous functions', () => {
// at 250ms they should be already resolved // at 250ms they should be already resolved
const t250 = postpone(250, () => { const t250 = postpone(250, () => {
expect(document.getElementById(RESULT_ID).innerHTML).toBe('false2'); expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('false2');
}); });
await Promise.all([t150, t250]); await Promise.all([t150, t250]);
}); });
it('call async action by then', async () => { it('call async action by then', async () => {
// @ts-ignore
jest.useFakeTimers(); jest.useFakeTimers();
let globalStore; let globalStore;
@ -150,12 +154,13 @@ describe('Asynchronous functions', () => {
// call async action by then // call async action by then
globalStore.$queue.increment().then(() => { globalStore.$queue.increment().then(() => {
expect(document.getElementById(RESULT_ID).innerHTML).toBe('false1'); expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('false1');
}); });
expect(document.getElementById(RESULT_ID).innerHTML).toBe('false0'); expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('false0');
// past 150 ms // past 150 ms
// @ts-ignore
jest.advanceTimersByTime(150); jest.advanceTimersByTime(150);
}); });
}); });

View File

@ -1,63 +0,0 @@
import * as Horizon from '@cloudsop/horizon/index.ts';
import { triggerClickEvent } from '../../jest/commonComponents';
import { useLogStore } from './store';
const { unmountComponentAtNode } = Horizon;
describe('Basic store manipulation', () => {
let container = null;
const BUTTON_ID = 'btn';
const RESULT_ID = 'result';
beforeEach(() => {
container = document.createElement('div');
document.body.appendChild(container);
});
afterEach(() => {
unmountComponentAtNode(container);
container.remove();
container = null;
});
it('Should use getters', () => {
function App() {
const logStore = useLogStore();
return <div id={RESULT_ID}>{logStore.length}</div>;
}
Horizon.render(<App />, container);
expect(document.getElementById(RESULT_ID).innerHTML).toBe('1');
});
it('Should use actions and update components', () => {
function App() {
const logStore = useLogStore();
return (
<div>
<button
id={BUTTON_ID}
onClick={() => {
logStore.addLog('a');
}}
>
add
</button>
<p id={RESULT_ID}>{logStore.length}</p>
</div>
);
}
Horizon.render(<App />, container);
Horizon.act(() => {
triggerClickEvent(container, BUTTON_ID);
});
expect(document.getElementById(RESULT_ID).innerHTML).toBe('2');
});
});

View File

@ -0,0 +1,155 @@
//@ts-ignore
import Horizon from '@cloudsop/horizon/index.ts';
import { triggerClickEvent } from '../../jest/commonComponents';
import { useLogStore } from './store';
import { describe, beforeEach, afterEach, it, expect } from '@jest/globals';
import { createStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler';
const { unmountComponentAtNode } = Horizon;
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 <div id={RESULT_ID}>{logStore.length}</div>;
}
Horizon.render(<App />, container);
expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('1');
});
it('Should use actions and update components', () => {
function App() {
const logStore = useLogStore();
return (
<div>
<button
id={BUTTON_ID}
onClick={() => {
logStore.addLog('a');
}}
>
add
</button>
<p id={RESULT_ID}>{logStore.length}</p>
</div>
);
}
Horizon.render(<App />, container);
Horizon.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 (
<div>
<button
id={BUTTON_ID}
onClick={() => {
incrementStore.doublePlusOne();
}}
>
+
</button>
<p id={RESULT_ID}>{incrementStore.count}</p>
</div>
);
}
Horizon.render(<App />, container);
Horizon.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 (
<div>
<button
id={BUTTON_ID}
onClick={() => {
incrementStore.doublePlusOne();
}}
>
+
</button>
<p id={RESULT_ID}>{incrementStore.count}</p>
</div>
);
}
Horizon.render(<App />, container);
Horizon.act(() => {
triggerClickEvent(container, BUTTON_ID);
});
expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('5');
})
});

View File

@ -1,11 +1,13 @@
//@ts-ignore
import * as Horizon from '@cloudsop/horizon/index.ts'; import * as Horizon from '@cloudsop/horizon/index.ts';
import { triggerClickEvent } from '../../jest/commonComponents'; import { triggerClickEvent } from '../../jest/commonComponents';
import { useLogStore } from './store'; import { useLogStore } from './store';
import { describe, beforeEach, afterEach, it, expect } from '@jest/globals';
const { unmountComponentAtNode } = Horizon; const { unmountComponentAtNode } = Horizon;
describe('Dollar store access', () => { describe('Dollar store access', () => {
let container = null; let container: HTMLElement | null = null;
const BUTTON_ID = 'btn'; const BUTTON_ID = 'btn';
const RESULT_ID = 'result'; const RESULT_ID = 'result';
@ -17,23 +19,23 @@ describe('Dollar store access', () => {
afterEach(() => { afterEach(() => {
unmountComponentAtNode(container); unmountComponentAtNode(container);
container.remove(); container?.remove();
container = null; container = null;
}); });
it('Should use $state and $computed', () => { it('Should use $s and $c', () => {
function App() { function App() {
const logStore = useLogStore(); const logStore = useLogStore();
return <div id={RESULT_ID}>{logStore.$computed.length()}</div>; return <div id={RESULT_ID}>{logStore.$c.length()}</div>;
} }
Horizon.render(<App />, container); Horizon.render(<App />, container);
expect(document.getElementById(RESULT_ID).innerHTML).toBe('1'); expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('1');
}); });
it('Should use $actions and update components', () => { it('Should use $a and update components', () => {
function App() { function App() {
const logStore = useLogStore(); const logStore = useLogStore();
@ -42,12 +44,12 @@ describe('Dollar store access', () => {
<button <button
id={BUTTON_ID} id={BUTTON_ID}
onClick={() => { onClick={() => {
logStore.$actions.addLog(); logStore.$a.addLog('data');
}} }}
> >
add add
</button> </button>
<p id={RESULT_ID}>{logStore.$computed.length()}</p> <p id={RESULT_ID}>{logStore.$c.length()}</p>
</div> </div>
); );
} }
@ -58,6 +60,6 @@ describe('Dollar store access', () => {
triggerClickEvent(container, BUTTON_ID); triggerClickEvent(container, BUTTON_ID);
}); });
expect(document.getElementById(RESULT_ID).innerHTML).toBe('2'); expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('2');
}); });
}); });

Some files were not shown because too many files have changed in this diff Show More