Match-id-1e4919b0867dd1554cb4c1d2558d8066882e71f6
This commit is contained in:
commit
bdd2ad3cce
|
@ -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
|
|
@ -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();
|
|
@ -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
|
|
@ -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 修复异步更新时路径错误
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -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
|
|
|
@ -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;
|
|
|
@ -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;
|
|
|
@ -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;
|
|
|
@ -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'));
|
|
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
|
|
@ -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',
|
||||||
|
|
|
@ -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'],
|
|
||||||
};
|
|
||||||
};
|
|
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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 的行为从而展开测试工作。
|
|
||||||
|
|
|
@ -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'});
|
|
||||||
});
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
.search {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
|
@ -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/)'}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
.container {
|
|
||||||
position: relative;
|
|
||||||
overflow-y: auto;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item {
|
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,2 +0,0 @@
|
||||||
export { VList } from './VList';
|
|
||||||
export type { renderInfoType } from './VList';
|
|
|
@ -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%);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
|
|
@ -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;
|
|
|
@ -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'});
|
|
||||||
}
|
|
||||||
);
|
|
|
@ -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);
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
import { createContext } from 'horizon';
|
|
||||||
|
|
||||||
export const MockContext = createContext({value: 'default context value'});
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -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);
|
|
|
@ -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>
|
|
|
@ -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,
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -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();
|
|
|
@ -1,7 +0,0 @@
|
||||||
chrome.devtools.panels.create('Horizon',
|
|
||||||
'',
|
|
||||||
'panel.html',
|
|
||||||
function(panel) {
|
|
||||||
|
|
||||||
}
|
|
||||||
);
|
|
|
@ -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>
|
|
|
@ -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>"]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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 的 name,userKey,indentation 属性不会发生变化
|
|
||||||
// 但是在跳转到新页面时,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;
|
|
|
@ -1,7 +0,0 @@
|
||||||
import { render } from 'horizon';
|
|
||||||
import App from './App';
|
|
||||||
|
|
||||||
render(
|
|
||||||
<App />,
|
|
||||||
document.getElementById('root')
|
|
||||||
);
|
|
|
@ -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>
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
export * from './PanelConnection';
|
|
|
@ -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,
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -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;
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -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';
|
|
|
@ -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);
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
|
@ -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"
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -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;
|
|
|
@ -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',
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
};
|
|
|
@ -1 +0,0 @@
|
||||||
# horizon jsx babel plugin
|
|
|
@ -1,7 +0,0 @@
|
||||||
/* istanbul ignore next */
|
|
||||||
module.exports = {
|
|
||||||
presets: [
|
|
||||||
'@babel/preset-env',
|
|
||||||
'@babel/preset-typescript'
|
|
||||||
]
|
|
||||||
};
|
|
|
@ -1,5 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
transform: {
|
|
||||||
'\\.(js|jsx|ts|tsx)$': 'babel-jest',
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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]));
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
var x = <div></div>;
|
|
||||||
var y = <Eview.Table></Eview.Table>;
|
|
||||||
var z = <this></this>;
|
|
|
@ -1,3 +0,0 @@
|
||||||
var x = Horizon.jsx('div');
|
|
||||||
var y = Horizon.jsx(Eview.Table);
|
|
||||||
var z = Horizon.jsx(this);
|
|
|
@ -1 +0,0 @@
|
||||||
<div className="123"></div>
|
|
|
@ -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
|
|
||||||
});
|
|
|
@ -1,9 +0,0 @@
|
||||||
{
|
|
||||||
"extends": "../../tsconfig.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"outDir": "./dist"
|
|
||||||
},
|
|
||||||
"include": [
|
|
||||||
"src"
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -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",
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
// custom事件自定义属性
|
constructor(customEventName: string, nativeEvtName: string, nativeEvent: AnyNativeEvent) {
|
||||||
nativeEvent.customEventName = customEventName;
|
for (const name in nativeEvent) {
|
||||||
nativeEvent.nativeEvent = nativeEvent;
|
this[name] = nativeEvent[name];
|
||||||
// 保存原生的事件类型,因为下面会修改
|
}
|
||||||
nativeEvent.nativeEventType = nativeEvent.type;
|
// stopPropagation和preventDefault 必须通过Event实例调用
|
||||||
|
this.stopPropagation = () => nativeEvent.stopPropagation();
|
||||||
|
this.preventDefault = () => nativeEvent.preventDefault();
|
||||||
|
|
||||||
Object.defineProperty(nativeEvent, 'type', { writable: true });
|
// custom事件自定义属性
|
||||||
nativeEvent.type = nativeEvtName;
|
this.customEventName = customEventName;
|
||||||
|
this.nativeEvent = nativeEvent;
|
||||||
|
// 保存原生的事件类型,因为下面会修改
|
||||||
|
this.nativeEventType = nativeEvent.type;
|
||||||
|
|
||||||
const orgKey = nativeEvent.key;
|
this.type = nativeEvtName;
|
||||||
Object.defineProperty(nativeEvent, 'key', { writable: true });
|
|
||||||
nativeEvent.key = uniqueKeyMap.get(orgKey) || orgKey;
|
|
||||||
|
|
||||||
return nativeEvent;
|
// 兼容IE的event key
|
||||||
|
const orgKey = (nativeEvent as any).key;
|
||||||
|
this.key = uniqueKeyMap.get(orgKey) || orgKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
isDefaultPrevented(): boolean {
|
||||||
|
return this.nativeEvent.defaultPrevented;
|
||||||
|
}
|
||||||
|
|
||||||
|
isPropagationStopped(): boolean {
|
||||||
|
return this.nativeEvent.cancelBubble;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建普通自定义事件对象实例,和原生事件对应
|
||||||
|
export function decorateNativeEvent(
|
||||||
|
customEventName: string,
|
||||||
|
nativeEvtName: string,
|
||||||
|
nativeEvent: AnyNativeEvent
|
||||||
|
): WrappedEvent {
|
||||||
|
return new WrappedEvent(customEventName, nativeEvtName, nativeEvent);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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>;
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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) => (
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
// TODO: implement vNode type
|
// TODO: implement vNode type
|
||||||
|
|
||||||
import {IObserver} from '../types';
|
import {IObserver} from './Observer';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 一个对象(对象、数组、集合)对应一个Observer
|
* 一个对象(对象、数组、集合)对应一个Observer
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -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]') {
|
||||||
|
@ -35,30 +88,30 @@ export function createStore<S extends object,A extends UserActions<S>,C extends
|
||||||
}
|
}
|
||||||
|
|
||||||
const proxyObj = createProxy(config.state, !config.options?.suppressHooks);
|
const proxyObj = createProxy(config.state, !config.options?.suppressHooks);
|
||||||
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
},
|
},
|
||||||
|
|
|
@ -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 />`;
|
|
|
@ -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');
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -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');
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,45 +1,55 @@
|
||||||
|
//@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';
|
||||||
|
|
||||||
|
const useUserStore = createStore({
|
||||||
|
id: 'user',
|
||||||
|
state: {
|
||||||
|
type: 'bing dun dun',
|
||||||
|
persons: new Map([
|
||||||
|
['p1', 1],
|
||||||
|
['p2', 2],
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
addOnePerson: (state, person) => {
|
||||||
|
state.persons.set(person.name, person.age);
|
||||||
|
},
|
||||||
|
delOnePerson: (state, person) => {
|
||||||
|
state.persons.delete(person.name);
|
||||||
|
},
|
||||||
|
clearPersons: state => {
|
||||||
|
state.persons.clear();
|
||||||
|
},
|
||||||
|
reset: state => {
|
||||||
|
state.persons = new Map([
|
||||||
|
['p1', 1],
|
||||||
|
['p2', 2],
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
describe('测试store中的Map', () => {
|
describe('测试store中的Map', () => {
|
||||||
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');
|
||||||
document.body.appendChild(container);
|
document.body.appendChild(container);
|
||||||
|
|
||||||
const persons = new Map([
|
useUserStore().reset();
|
||||||
['p1', 1],
|
|
||||||
['p2', 2],
|
|
||||||
]);
|
|
||||||
|
|
||||||
createStore({
|
|
||||||
id: 'user',
|
|
||||||
state: {
|
|
||||||
type: 'bing dun dun',
|
|
||||||
persons: persons,
|
|
||||||
},
|
|
||||||
actions: {
|
|
||||||
addOnePerson: (state, person) => {
|
|
||||||
state.persons.set(person.name, person.age);
|
|
||||||
},
|
|
||||||
delOnePerson: (state, person) => {
|
|
||||||
state.persons.delete(person.name);
|
|
||||||
},
|
|
||||||
clearPersons: state => {
|
|
||||||
state.persons.clear();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
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: ');
|
||||||
});
|
});
|
||||||
});
|
});
|
|
@ -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');
|
||||||
});
|
});
|
||||||
});
|
});
|
|
@ -1,53 +1,62 @@
|
||||||
|
//@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';
|
||||||
|
|
||||||
|
const useUserStore = createStore({
|
||||||
|
id: 'user',
|
||||||
|
state: {
|
||||||
|
type: 'bing dun dun',
|
||||||
|
persons: new Set([
|
||||||
|
{ name: 'p1', age: 1 },
|
||||||
|
{ name: 'p2', age: 2 },
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
addOnePerson: (state, person) => {
|
||||||
|
state.persons.add(person);
|
||||||
|
},
|
||||||
|
delOnePerson: (state, person) => {
|
||||||
|
state.persons.delete(person);
|
||||||
|
},
|
||||||
|
clearPersons: state => {
|
||||||
|
state.persons.clear();
|
||||||
|
},
|
||||||
|
reset: state => {
|
||||||
|
state.persons = new Set([
|
||||||
|
{ name: 'p1', age: 1 },
|
||||||
|
{ name: 'p2', age: 2 },
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
describe('测试store中的Set', () => {
|
describe('测试store中的Set', () => {
|
||||||
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');
|
||||||
document.body.appendChild(container);
|
document.body.appendChild(container);
|
||||||
|
|
||||||
const persons = new Set([
|
useUserStore().reset();
|
||||||
{ name: 'p1', age: 1 },
|
|
||||||
{ name: 'p2', age: 2 },
|
|
||||||
]);
|
|
||||||
|
|
||||||
createStore({
|
|
||||||
id: 'user',
|
|
||||||
state: {
|
|
||||||
type: 'bing dun dun',
|
|
||||||
persons: persons,
|
|
||||||
},
|
|
||||||
actions: {
|
|
||||||
addOnePerson: (state, person) => {
|
|
||||||
state.persons.add(person);
|
|
||||||
},
|
|
||||||
delOnePerson: (state, person) => {
|
|
||||||
state.persons.delete(person);
|
|
||||||
},
|
|
||||||
clearPersons: state => {
|
|
||||||
state.persons.clear();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
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: ');
|
||||||
});
|
});
|
||||||
});
|
});
|
|
@ -1,45 +1,55 @@
|
||||||
|
//@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';
|
||||||
|
|
||||||
|
const useUserStore = createStore({
|
||||||
|
id: 'user',
|
||||||
|
state: {
|
||||||
|
type: 'bing dun dun',
|
||||||
|
persons: new WeakMap([
|
||||||
|
[{ name: 'p1' }, 1],
|
||||||
|
[{ name: 'p2' }, 2],
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
addOnePerson: (state, person) => {
|
||||||
|
state.persons.set(person, 3);
|
||||||
|
},
|
||||||
|
delOnePerson: (state, person) => {
|
||||||
|
state.persons.delete(person);
|
||||||
|
},
|
||||||
|
clearPersons: state => {
|
||||||
|
state.persons = new WeakMap([]);
|
||||||
|
},
|
||||||
|
reset: state => {
|
||||||
|
state.persons = new WeakMap([
|
||||||
|
[{ name: 'p1' }, 1],
|
||||||
|
[{ name: 'p2' }, 2],
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
describe('测试store中的WeakMap', () => {
|
describe('测试store中的WeakMap', () => {
|
||||||
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');
|
||||||
document.body.appendChild(container);
|
document.body.appendChild(container);
|
||||||
|
|
||||||
const persons = new WeakMap([
|
useUserStore().reset();
|
||||||
[{ name: 'p1' }, 1],
|
|
||||||
[{ name: 'p2' }, 2],
|
|
||||||
]);
|
|
||||||
|
|
||||||
createStore({
|
|
||||||
id: 'user',
|
|
||||||
state: {
|
|
||||||
type: 'bing dun dun',
|
|
||||||
persons: persons,
|
|
||||||
},
|
|
||||||
actions: {
|
|
||||||
addOnePerson: (state, person) => {
|
|
||||||
state.persons.set(person, 3);
|
|
||||||
},
|
|
||||||
delOnePerson: (state, person) => {
|
|
||||||
state.persons.delete(person);
|
|
||||||
},
|
|
||||||
clearPersons: state => {
|
|
||||||
state.persons.clear();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
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');
|
||||||
});
|
});
|
||||||
});
|
});
|
|
@ -1,53 +1,62 @@
|
||||||
|
//@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';
|
||||||
|
|
||||||
|
const useUserStore = createStore({
|
||||||
|
id: 'user',
|
||||||
|
state: {
|
||||||
|
type: 'bing dun dun',
|
||||||
|
persons: new WeakSet([
|
||||||
|
{ name: 'p1', age: 1 },
|
||||||
|
{ name: 'p2', age: 2 },
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
addOnePerson: (state, person) => {
|
||||||
|
state.persons.add(person);
|
||||||
|
},
|
||||||
|
delOnePerson: (state, person) => {
|
||||||
|
state.persons.delete(person);
|
||||||
|
},
|
||||||
|
clearPersons: state => {
|
||||||
|
state.persons = new WeakSet([]);
|
||||||
|
},
|
||||||
|
reset: state => {
|
||||||
|
state.persons = new WeakSet([
|
||||||
|
{ name: 'p1', age: 1 },
|
||||||
|
{ name: 'p2', age: 2 },
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
describe('测试store中的WeakSet', () => {
|
describe('测试store中的WeakSet', () => {
|
||||||
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');
|
||||||
document.body.appendChild(container);
|
document.body.appendChild(container);
|
||||||
|
|
||||||
const persons = new WeakSet([
|
useUserStore().reset();
|
||||||
{ name: 'p1', age: 1 },
|
|
||||||
{ name: 'p2', age: 2 },
|
|
||||||
]);
|
|
||||||
|
|
||||||
createStore({
|
|
||||||
id: 'user',
|
|
||||||
state: {
|
|
||||||
type: 'bing dun dun',
|
|
||||||
persons: persons,
|
|
||||||
},
|
|
||||||
actions: {
|
|
||||||
addOnePerson: (state, person) => {
|
|
||||||
state.persons.add(person);
|
|
||||||
},
|
|
||||||
delOnePerson: (state, person) => {
|
|
||||||
state.persons.delete(person);
|
|
||||||
},
|
|
||||||
clearPersons: state => {
|
|
||||||
state.persons.clear();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
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');
|
||||||
});
|
});
|
||||||
});
|
});
|
|
@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
|
@ -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');
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -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');
|
||||||
|
})
|
||||||
|
});
|
|
@ -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
Loading…
Reference in New Issue