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;