diff --git a/jest.config.js b/jest.config.js
index 3d280082..333fc68e 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -130,7 +130,7 @@ module.exports = {
//setupFiles: [],
// A list of paths to modules that run some code to configure or set up the testing framework before each test
- setupFilesAfterEnv: [require.resolve('./scripts/__tests__/jest/deployConfiguration.js')],
+ setupFilesAfterEnv: [require.resolve('./scripts/__tests__/jest/jestSetting.js')],
// A list of paths to snapshot serializer modules Jest should use for snapshot testing
// snapshotSerializers: [],
diff --git a/libs/horizon/src/renderer/taskExecutor/TaskExecutor.ts b/libs/horizon/src/renderer/taskExecutor/TaskExecutor.ts
index 6f8e98e8..15a8c147 100644
--- a/libs/horizon/src/renderer/taskExecutor/TaskExecutor.ts
+++ b/libs/horizon/src/renderer/taskExecutor/TaskExecutor.ts
@@ -62,7 +62,7 @@ function callTasks() {
}
}
-function runAsync(callback, priorityLevel= NormalPriority ) {
+function runAsync(callback, priorityLevel= NormalPriority) {
let increment;
switch (priorityLevel) {
case ImmediatePriority:
diff --git a/scripts/__tests__/ComponentTest/SimpleUseHook.test.js b/scripts/__tests__/ComponentTest/SimpleUseHook.test.js
index 6155b962..0d99f8ed 100644
--- a/scripts/__tests__/ComponentTest/SimpleUseHook.test.js
+++ b/scripts/__tests__/ComponentTest/SimpleUseHook.test.js
@@ -1,6 +1,6 @@
import * as React from '../../../libs/horizon/src/external/Horizon';
import * as HorizonDOM from '../../../libs/horizon/src/dom/DOMExternal';
-import { act } from 'react-dom/test-utils';
+import { act } from '../jest/customMatcher';
describe('Hook Test', () => {
diff --git a/scripts/__tests__/ComponentTest/UseEffect.test.js b/scripts/__tests__/ComponentTest/UseEffect.test.js
new file mode 100644
index 00000000..0eccd5f9
--- /dev/null
+++ b/scripts/__tests__/ComponentTest/UseEffect.test.js
@@ -0,0 +1,442 @@
+import * as React from '../../../libs/horizon/src/external/Horizon';
+import * as HorizonDOM from '../../../libs/horizon/src/dom/DOMExternal';
+import * as LogUtils from '../jest/logUtils';
+import { act } from '../jest/customMatcher';
+
+describe('useEffect Hook Test', () => {
+ const { useEffect, useLayoutEffect, useState, memo } = React;
+ const { unmountComponentAtNode } = HorizonDOM;
+ let container = null;
+ beforeEach(() => {
+ LogUtils.clear();
+ // 创建一个 DOM 元素作为渲染目标
+ container = document.createElement('div');
+ document.body.appendChild(container);
+ });
+
+ afterEach(() => {
+ // 退出时进行清理
+ unmountComponentAtNode(container);
+ container.remove();
+ container = null;
+ LogUtils.clear();
+ });
+
+ const Text = (props) => {
+ LogUtils.log(props.text);
+ return
{props.text}
;
+ }
+
+ it('兄弟节点被删除,useEffect依然正常', () => {
+ const App = () => {
+ return ;
+ }
+ const NewApp = () => {
+ useEffect(() => {
+ LogUtils.log(`NewApp effect`);
+ }, []);
+ return ;
+ }
+ const na = ;
+ // 必须设置key值,否则在diff的时候na会被视为不同组件
+ HorizonDOM.render([, na], container);
+ expect(LogUtils.getAndClear()).toEqual([
+ 'App',
+ 'NewApp'
+ ]);
+ expect(container.textContent).toBe('AppNewApp');
+ expect(LogUtils.getAndClear()).toEqual([]);
+ // 在执行新的render前,会执行完上一次render的useEffect,所以LogUtils会加入'NewApp effect'。
+ HorizonDOM.render([na], container);
+ expect(LogUtils.getAndClear()).toEqual(['NewApp effect']);
+ expect(container.textContent).toBe('NewApp');
+ expect(LogUtils.getAndClear()).toEqual([]);
+ });
+
+ it('兄弟节点更新,useEffect依然正常', () => {
+ const App = () => {
+ const [num, setNum] = useState(0);
+ useLayoutEffect(() => {
+ if (num === 0) {
+ setNum(1);
+ }
+ LogUtils.log('App Layout effect ' + num);
+ });
+ return ;
+ }
+ const NewApp = () => {
+ useEffect(() => {
+ LogUtils.log(`NewApp effect`);
+ }, []);
+ return ;
+ }
+ // 必须设置key值,否则在diff的时候na会被视为不同组件
+ HorizonDOM.render([, ], container);
+ expect(LogUtils.getAndClear()).toEqual([
+ 'App',
+ 'NewApp',
+ 'App Layout effect 0',
+ // 在App更新前,会执行完NewApp的useEffect
+ 'NewApp effect',
+ 'App',
+ 'App Layout effect 1',
+ ]);
+ expect(container.textContent).toBe('AppNewApp');
+ });
+
+ it('兄弟节点执行新的挂载动作,useEffect依然正常', () => {
+ const newContainer = document.createElement('div');
+ const App = () => {
+ useLayoutEffect(() => {
+ LogUtils.log('App Layout effect');
+ HorizonDOM.render(, newContainer);
+ });
+ return ;
+ }
+ const NewApp = () => {
+ useEffect(() => {
+ LogUtils.log(`NewApp effect`);
+ }, []);
+ return ;
+ }
+ // 必须设置key值,否则在diff的时候na会被视为不同组件
+ HorizonDOM.render([, ], container);
+ expect(LogUtils.getAndClear()).toEqual([
+ 'App',
+ 'NewApp',
+ 'App Layout effect',
+ // 在执行useLayoutEffectApp的render前,会执行完NewApp的useEffect
+ 'NewApp effect',
+ 'NewContainer',
+ ]);
+ expect(container.textContent).toBe('AppNewApp');
+ });
+
+ it('执行新render的useEffect前会先执行旧render的useEffect', () => {
+ const App = (props) => {
+ useEffect(() => {
+ LogUtils.log(`First effect [${props.num}]`);
+ });
+ return ;
+ }
+ act(() => {
+ HorizonDOM.render(, container, () => LogUtils.log('callback effect'));
+ expect(LogUtils.getAndClear()).toEqual(['num: 0', 'callback effect']);
+ expect(container.textContent).toEqual('num: 0');
+ HorizonDOM.render(, container, () => LogUtils.log('callback effect'));
+ // 执行新的render前,会执行旧render的useEffect,所以会添加'First effect [0]'
+ expect(LogUtils.getAndClear()).toEqual(['First effect [0]', 'num: 1', 'callback effect']);
+ expect(container.textContent).toEqual('num: 1');
+ })
+ // 最后在act执行完后会执行新render的useEffect
+ expect(LogUtils.getAndClear()).toEqual(['First effect [1]']);
+ });
+
+ it('混合使用useEffect', () => {
+ const App = (props) => {
+ useEffect(() => {
+ LogUtils.log(`First effect [${props.num}]`);
+ });
+ useEffect(() => {
+ LogUtils.log(`Second effect [${props.num}]`);
+ });
+ return ;
+ }
+ act(() => {
+ HorizonDOM.render(, container, () => LogUtils.log('callback effect'));
+ expect(LogUtils.getAndClear()).toEqual(['num: 0', 'callback effect']);
+ expect(container.textContent).toEqual('num: 0');
+ })
+ expect(LogUtils.getAndClear()).toEqual(['First effect [0]', 'Second effect [0]']);
+ act(() => {
+ HorizonDOM.render(, container, () => LogUtils.log('callback effect'));
+ expect(LogUtils.getAndClear()).toEqual(['num: 1', 'callback effect']);
+ expect(container.textContent).toEqual('num: 1');
+ })
+ expect(LogUtils.getAndClear()).toEqual(['First effect [1]', 'Second effect [1]']);
+ });
+
+ it('创建,销毁useEffect', () => {
+ const App = (props) => {
+ useEffect(() => {
+ LogUtils.log(`num effect [${props.num}]`);
+ return () => {
+ LogUtils.log('num effect destroy');
+ };
+ }, [props.num]);
+ useEffect(() => {
+ LogUtils.log(`word effect [${props.word}]`);
+ return () => {
+ LogUtils.log('word effect destroy');
+ };
+ }, [props.word]);
+ useLayoutEffect(() => {
+ LogUtils.log(`num Layouteffect [${props.num}]`);
+ return () => {
+ LogUtils.log('num Layouteffect destroy');
+ };
+ }, [props.num]);
+ useLayoutEffect(() => {
+ LogUtils.log(`word Layouteffect [${props.word}]`);
+ return () => {
+ LogUtils.log('word Layouteffect destroy');
+ };
+ }, [props.word]);
+ return ;
+ }
+
+ act(() => {
+ HorizonDOM.render(, container, () => LogUtils.log('callback effect'));
+ expect(LogUtils.getAndClear()).toEqual([
+ 'num: 0,word: App',
+ 'num Layouteffect [0]',
+ 'word Layouteffect [App]',
+ 'callback effect'
+ ]);
+ })
+ expect(LogUtils.getAndClear()).toEqual([
+ 'num effect [0]',
+ 'word effect [App]',
+ ]);
+
+ act(() => {
+ // 此时word改变,num不变
+ HorizonDOM.render(, container, () => LogUtils.log('callback effect'));
+ expect(LogUtils.getAndClear()).toEqual([
+ 'num: 0,word: React',
+ 'word Layouteffect destroy',
+ 'word Layouteffect [React]',
+ 'callback effect'
+ ]);
+ });
+ expect(LogUtils.getAndClear()).toEqual([
+ 'word effect destroy',
+ 'word effect [React]',
+ ]);
+
+ act(() => {
+ // 此时num和word的所有effect都销毁
+ HorizonDOM.render(null, container, () => LogUtils.log('callback effect'));
+ expect(LogUtils.getAndClear()).toEqual([
+ 'num Layouteffect destroy',
+ 'word Layouteffect destroy',
+ 'callback effect'
+ ]);
+ });
+ expect(LogUtils.getAndClear()).toEqual([
+ 'num effect destroy',
+ 'word effect destroy',
+ ]);
+ });
+
+ it('销毁不含依赖数组的useEffect', () => {
+ const App = (props) => {
+ useEffect(() => {
+ LogUtils.log(`num effect [${props.num}]`);
+ return () => {
+ LogUtils.log('num effect destroy');
+ };
+ });
+ return ;
+ }
+
+ act(() => {
+ HorizonDOM.render(, container, () => LogUtils.log('callback effect'));
+ expect(LogUtils.getAndClear()).toEqual([
+ 'num: 0',
+ 'callback effect'
+ ]);
+ expect(container.textContent).toEqual('num: 0');
+ })
+ expect(LogUtils.getAndClear()).toEqual([
+ 'num effect [0]',
+ ]);
+
+ act(() => {
+ HorizonDOM.render(, container, () => LogUtils.log('callback effect'));
+ expect(LogUtils.getAndClear()).toEqual([
+ 'num: 1',
+ 'callback effect'
+ ]);
+ expect(container.textContent).toEqual('num: 1');
+ });
+ expect(LogUtils.getAndClear()).toEqual([
+ 'num effect destroy',
+ 'num effect [1]',
+ ]);
+
+ act(() => {
+ HorizonDOM.render(null, container, () => LogUtils.log('callback effect'));
+ expect(LogUtils.getAndClear()).toEqual([
+ 'callback effect'
+ ]);
+ expect(container.textContent).toEqual('');
+ });
+ expect(LogUtils.getAndClear()).toEqual([
+ 'num effect destroy'
+ ]);
+ });
+
+ it('销毁依赖空数组的useEffect', () => {
+ const App = (props) => {
+ useEffect(() => {
+ LogUtils.log(`num effect [${props.num}]`);
+ return () => {
+ LogUtils.log('num effect destroy');
+ };
+ }, []);
+ return ;
+ }
+
+ act(() => {
+ HorizonDOM.render(, container, () => LogUtils.log('callback effect'));
+ expect(LogUtils.getAndClear()).toEqual([
+ 'num: 0',
+ 'callback effect'
+ ]);
+ expect(container.textContent).toEqual('num: 0');
+ })
+ expect(LogUtils.getAndClear()).toEqual([
+ 'num effect [0]',
+ ]);
+
+ act(() => {
+ HorizonDOM.render(, container, () => LogUtils.log('callback effect'));
+ expect(LogUtils.getAndClear()).toEqual([
+ 'num: 1',
+ 'callback effect'
+ ]);
+ expect(container.textContent).toEqual('num: 1');
+ });
+ // 没有执行useEffect
+ expect(LogUtils.getAndClear()).toEqual([]);
+
+ act(() => {
+ HorizonDOM.render(null, container, () => LogUtils.log('callback effect'));
+ expect(LogUtils.getAndClear()).toEqual([
+ 'callback effect'
+ ]);
+ expect(container.textContent).toEqual('');
+ });
+ expect(LogUtils.getAndClear()).toEqual([
+ 'num effect destroy'
+ ]);
+ });
+
+ it('useEffect里使用useState(1', () => {
+ let setNum;
+ const App = () => {
+ const [num, _setNum] = React.useState(0);
+ useEffect(() => {
+ LogUtils.log(`num effect [${num}]`);
+ setNum = () => _setNum(1);
+ }, [num]);
+ useLayoutEffect(() => {
+ LogUtils.log(`num Layouteffect [${num}]`);
+ return () => {
+ LogUtils.log('num Layouteffect destroy');
+ };
+ }, []);
+ return ;
+ }
+
+ act(() => {
+ HorizonDOM.render(, container, () => LogUtils.log('callback effect'));
+ expect(LogUtils.getAndClear()).toEqual([
+ 'num: 0',
+ 'num Layouteffect [0]',
+ 'callback effect'
+ ]);
+ })
+ expect(LogUtils.getAndClear()).toEqual([
+ 'num effect [0]',
+ ]);
+
+ act(() => {
+ setNum();
+ expect(LogUtils.getAndClear()).toEqual([
+ 'num: 1'
+ ]);
+ });
+ expect(LogUtils.getAndClear()).toEqual(['num effect [1]']);
+ });
+
+ it('useEffect里使用useState(2', () => {
+ let setNum;
+ const App = () => {
+ const [num, _setNum] = useState(0);
+ setNum = _setNum;
+ useEffect(() => {
+ LogUtils.log(`App effect`);
+ setNum(1);
+ }, []);
+ return ;
+ }
+
+ HorizonDOM.render(, container, () => LogUtils.log('Sync effect'));
+ expect(LogUtils.getAndClear()).toEqual(['Num: 0', 'Sync effect']);
+ expect(container.textContent).toEqual('Num: 0');
+ act(() => {
+ setNum(2);
+ });
+
+ // 虽然执行了setNum(2),但执行到setNum(1),所以最终num为1
+ expect(LogUtils.getAndClear()).toEqual([
+ 'App effect',
+ 'Num: 1',
+ ]);
+
+ expect(container.textContent).toEqual('Num: 1');
+ });
+
+ it('useEffect与memo一起使用', () => {
+ let setNum;
+ const App = memo(() => {
+ const [num, _setNum] = useState(0);
+ setNum = _setNum;
+ useEffect(() => {
+ LogUtils.log(`num effect [${num}]`);
+ return () => {
+ LogUtils.log(`num effect destroy ${num}`);
+ };
+ });
+ return ;
+ })
+ act(() => {
+ HorizonDOM.render(, container, () => LogUtils.log('callback effect'));
+ expect(LogUtils.getAndClear()).toEqual([
+ 0,
+ 'callback effect'
+ ]);
+ expect(container.textContent).toEqual('0');
+ });
+ expect(LogUtils.getAndClear()).toEqual(['num effect [0]']);
+
+ // 不会重新渲染
+ act(() => {
+ HorizonDOM.render(, container, () => LogUtils.log('callback effect'));
+ expect(LogUtils.getAndClear()).toEqual(['callback effect']);
+ expect(container.textContent).toEqual('0');
+ });
+ expect(LogUtils.getAndClear()).toEqual([]);
+
+ // 会重新渲染
+ act(() => {
+ setNum(1);
+ expect(LogUtils.getAndClear()).toEqual([1]);
+ expect(container.textContent).toEqual('1');
+ });
+ expect(LogUtils.getAndClear()).toEqual([
+ 'num effect destroy 0',
+ 'num effect [1]'
+ ]);
+
+ act(() => {
+ HorizonDOM.render(null, container, () => LogUtils.log('callback effect'));
+ expect(LogUtils.getAndClear()).toEqual(['callback effect']);
+ expect(container.textContent).toEqual('');
+ });
+ expect(LogUtils.getAndClear()).toEqual(['num effect destroy 1']);
+ });
+
+})
diff --git a/scripts/__tests__/ComponentTest/UseState.test.js b/scripts/__tests__/ComponentTest/UseState.test.js
index 998221bd..ffd1cda0 100644
--- a/scripts/__tests__/ComponentTest/UseState.test.js
+++ b/scripts/__tests__/ComponentTest/UseState.test.js
@@ -79,10 +79,10 @@ describe('useState Hook Test', () => {
}
HorizonDOM.render(, container);
expect(container.querySelector('p').innerHTML).toBe('0');
- expect(LogUtils).toMatchValue([0]);
+ expect(LogUtils.getAndClear()).toEqual([0]);
// useState修改state 时,设置相同的值,函数组件不会重新渲染
setNum(0);
- expect(LogUtils).toMatchValue([]);
+ expect(LogUtils.getAndClear()).toEqual([]);
expect(container.querySelector('p').innerHTML).toBe('0');
});
@@ -98,12 +98,12 @@ describe('useState Hook Test', () => {
})
const ref = React.createRef(null);
HorizonDOM.render(, container);
- expect(LogUtils).toMatchValue([1]);
+ expect(LogUtils.getAndClear()).toEqual([1]);
expect(container.querySelector('p').innerHTML).toBe('1');
// 设置num为3
ref.current.setNum(3);
// 初始化函数只在初始渲染时被调用,所以Scheduler里的dataArray清空后没有新增。
- expect(LogUtils).toMatchValue([]);
+ expect(LogUtils.getAndClear()).toEqual([]);
expect(container.querySelector('p').innerHTML).toBe('3');
});
@@ -115,15 +115,15 @@ describe('useState Hook Test', () => {
return ;
})
HorizonDOM.render(, container);
- expect(LogUtils).toMatchValue([0]);
+ expect(LogUtils.getAndClear()).toEqual([0]);
expect(container.querySelector('p').innerHTML).toBe('0');
// 不会重新渲染
HorizonDOM.render(, container);
- expect(LogUtils).toMatchValue([]);
+ expect(LogUtils.getAndClear()).toEqual([]);
expect(container.querySelector('p').innerHTML).toBe('0');
// 会重新渲染
setNum(1)
- expect(LogUtils).toMatchValue([1]);
+ expect(LogUtils.getAndClear()).toEqual([1]);
expect(container.querySelector('p').innerHTML).toBe('1');
});
});
diff --git a/scripts/__tests__/jest/customMatcher.js b/scripts/__tests__/jest/customMatcher.js
index 4fa382b3..c6b80208 100644
--- a/scripts/__tests__/jest/customMatcher.js
+++ b/scripts/__tests__/jest/customMatcher.js
@@ -1,3 +1,6 @@
+import { runAsyncEffects } from '../../../libs/horizon/src/renderer/submit/HookEffectHandler'
+import { syncUpdates } from '../../../libs/horizon/src/renderer/TreeBuilder'
+
function runAssertion(fn) {
try {
fn();
@@ -7,7 +10,7 @@ function runAssertion(fn) {
message: () => error.message,
};
}
- return {pass: true};
+ return { pass: true };
}
function toMatchValue(LogUtils, expectedValues) {
@@ -17,6 +20,12 @@ function toMatchValue(LogUtils, expectedValues) {
});
}
+const act = (fun) => {
+ syncUpdates(fun);
+ runAsyncEffects();
+}
+
module.exports = {
toMatchValue,
+ act
};
diff --git a/scripts/__tests__/jest/logUtils.js b/scripts/__tests__/jest/logUtils.js
index 5ee34798..276bfb92 100644
--- a/scripts/__tests__/jest/logUtils.js
+++ b/scripts/__tests__/jest/logUtils.js
@@ -18,7 +18,7 @@ const getAndClear = () => {
};
const clear = () => {
- dataArray = null;
+ dataArray = dataArray ? null : dataArray;
}
exports.clear = clear;