From 7dce0e69e3c334acf1320c6a240c2a2ac3a3f9e8 Mon Sep 17 00:00:00 2001 From: * <8> Date: Tue, 8 Mar 2022 09:57:37 +0800 Subject: [PATCH 1/5] Match-id-dcf83e9762d769d5a52954b612df113c9bf42253 --- scripts/__tests__/jest/customMatcher.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/scripts/__tests__/jest/customMatcher.js b/scripts/__tests__/jest/customMatcher.js index c6b80208..f921e916 100644 --- a/scripts/__tests__/jest/customMatcher.js +++ b/scripts/__tests__/jest/customMatcher.js @@ -1,5 +1,5 @@ -import { runAsyncEffects } from '../../../libs/horizon/src/renderer/submit/HookEffectHandler' -import { syncUpdates } from '../../../libs/horizon/src/renderer/TreeBuilder' +import { runAsyncEffects } from '../../../libs/horizon/src/renderer/submit/HookEffectHandler'; +import { callRenderQueueImmediate } from '../../../libs/horizon/src/renderer/taskExecutor/RenderQueue'; function runAssertion(fn) { try { @@ -21,7 +21,8 @@ function toMatchValue(LogUtils, expectedValues) { } const act = (fun) => { - syncUpdates(fun); + fun(); + callRenderQueueImmediate(); runAsyncEffects(); } From 5a06b9b663a5eb2f5de9326e84f6c9f978039078 Mon Sep 17 00:00:00 2001 From: * <8> Date: Sun, 13 Mar 2022 13:11:36 +0800 Subject: [PATCH 2/5] Match-id-1988199472c662613fee402f6bd87513d3016c2b --- .../HooksExceptEffectAndState.test.js | 249 +++++++++++ .../__tests__/ComponentTest/UseEffect.test.js | 393 +++++++++++++++--- .../__tests__/ComponentTest/UseState.test.js | 42 ++ scripts/__tests__/jest/customMatcher.js | 6 +- 4 files changed, 636 insertions(+), 54 deletions(-) create mode 100644 scripts/__tests__/ComponentTest/HooksExceptEffectAndState.test.js diff --git a/scripts/__tests__/ComponentTest/HooksExceptEffectAndState.test.js b/scripts/__tests__/ComponentTest/HooksExceptEffectAndState.test.js new file mode 100644 index 00000000..34bb86d0 --- /dev/null +++ b/scripts/__tests__/ComponentTest/HooksExceptEffectAndState.test.js @@ -0,0 +1,249 @@ +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('Hook Test', () => { + const { useMemo, useRef, useState, useImperativeHandle, forwardRef, useLayoutEffect, useEffect } = 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}

; + } + + describe('useLayoutEffect Test', () => { + it('useLayoutEffect的触发时序', () => { + const App = (props) => { + useLayoutEffect(() => { + LogUtils.log('LayoutEffect'); + }); + return ; + } + HorizonDOM.render(, container, () => LogUtils.log('Sync effect')); + expect(LogUtils.getAndClear()).toEqual([ + 1, + // 同步在渲染之后 + 'LayoutEffect', + 'Sync effect' + ]); + expect(container.querySelector('p').innerHTML).toBe('1'); + // 更新 + HorizonDOM.render(, container, () => LogUtils.log('Sync effect')); + expect(LogUtils.getAndClear()).toEqual([ + 2, + 'LayoutEffect', + 'Sync effect' + ]); + expect(container.querySelector('p').innerHTML).toBe('2'); + }); + + it('创建,销毁useLayoutEffect', () => { + const App = (props) => { + useEffect(() => { + LogUtils.log(`num effect [${props.num}]`); + return () => { + LogUtils.log('num effect destroy'); + }; + }, [props.num]); + useLayoutEffect(() => { + LogUtils.log(`num Layouteffect [${props.num}]`); + return () => { + LogUtils.log(`num [${props.num}] Layouteffect destroy`); + }; + }, [props.num]); + return ; + } + + act(() => { + HorizonDOM.render(, container, () => LogUtils.log('callback effect')); + expect(LogUtils.getAndClear()).toEqual([ + 'num: 0', + 'num Layouteffect [0]', + 'callback effect' + ]); + expect(container.textContent).toBe('num: 0'); + }) + + // 更新 + act(() => { + HorizonDOM.render(, container, () => LogUtils.log('callback effect')); + }) + expect(LogUtils.getAndClear()).toEqual([ + // 异步effect + 'num effect [0]', + 'num: 1', + // 旧Layouteffect销毁 + 'num [0] Layouteffect destroy', + // 新Layouteffect建立 + 'num Layouteffect [1]', + 'callback effect', + // 异步旧的effect销毁 + 'num effect destroy', + // 异步新的effect建立 + 'num effect [1]' + ]); + + act(() => { + HorizonDOM.render(null, container, () => LogUtils.log('callback effect')); + }) + expect(LogUtils.getAndClear()).toEqual([ + // 同步Layouteffect销毁 + 'num [1] Layouteffect destroy', + 'callback effect', + // 最后执行异步effect销毁 + 'num effect destroy', + ]); + }); + }) + + describe('useMemo Test', () => { + it('触发useMemo', () => { + const App = (props) => { + const num = useMemo(() => { + LogUtils.log(props._num); + return props._num + 1; + }, [props._num]); + return ; + } + HorizonDOM.render(, container); + expect(LogUtils.getAndClear()).toEqual([ + 0, + 1 + ]); + expect(container.textContent).toBe('1'); + + HorizonDOM.render(, container); + expect(LogUtils.getAndClear()).toEqual([ + 1, + 2 + ]); + expect(container.textContent).toBe('2'); + + HorizonDOM.render(, container); + // 不会触发useMemo + expect(LogUtils.getAndClear()).toEqual([2]); + expect(container.textContent).toBe('2'); + + HorizonDOM.render(, container); + expect(LogUtils.getAndClear()).toEqual([ + 2, + 3 + ]); + expect(container.textContent).toBe('3'); + }); + + it('输入不变,重新渲染也会触发useMemo', () => { + const App = (props) => { + const num = useMemo(props._num); + return ; + } + + const num1 = () => { + LogUtils.log('num 1'); + return 1; + } + + const num2 = () => { + LogUtils.log('num 2'); + return 2; + } + + HorizonDOM.render(, container); + expect(LogUtils.getAndClear()).toEqual(['num 1', 1]); + + HorizonDOM.render(, container); + expect(LogUtils.getAndClear()).toEqual(['num 1', 1]); + + HorizonDOM.render(, container); + expect(LogUtils.getAndClear()).toEqual(['num 1', 1]); + + HorizonDOM.render(, container); + expect(LogUtils.getAndClear()).toEqual(['num 2', 2]); + }); + }) + + describe('useRef Test', () => { + it('更新current值时不会re-render', () => { + const App = () => { + const ref = useRef(1); + return ( + <> + + ; + + ) + + } + HorizonDOM.render(, container); + expect(LogUtils.getAndClear()).toEqual([1]); + expect(container.querySelector('p').innerHTML).toBe('1'); + // 点击按钮触发ref.current加1 + container.querySelector('button').click(); + // ref.current改变不会触发重新渲染 + expect(LogUtils.getAndClear()).toEqual([]); + expect(container.querySelector('p').innerHTML).toBe('1'); + }); + }) + + describe('useImperativeHandle Test', () => { + it('useImperativeHandle没有配置dep时自动更新', () => { + + let App = (props, ref) => { + const [num, setNum] = useState(0); + useImperativeHandle(ref, () => ({ num, setNum })); + return ; + } + let App1 = (props, ref) => { + const [num1, setNum1] = useState(0); + useImperativeHandle(ref, () => ({ num1, setNum1 }), []); + return ; + } + + App = forwardRef(App); + App1 = forwardRef(App1); + const counter = React.createRef(null); + const counter1 = React.createRef(null); + HorizonDOM.render(, container); + expect(LogUtils.getAndClear()).toEqual([0]); + expect(counter.current.num).toBe(0); + act(() => { + counter.current.setNum(1); + }); + expect(LogUtils.getAndClear()).toEqual([1]); + // useImperativeHandle没有配置的dep,所以会自动更新 + expect(counter.current.num).toBe(1); + // 清空container + unmountComponentAtNode(container); + + HorizonDOM.render(, container); + expect(LogUtils.getAndClear()).toEqual([0]); + expect(counter1.current.num1).toBe(0); + act(() => { + counter1.current.setNum1(1); + }); + expect(LogUtils.getAndClear()).toEqual([1]); + // useImperativeHandle的dep为[],所以不会变 + expect(counter1.current.num1).toBe(0); + }); + }) +}) diff --git a/scripts/__tests__/ComponentTest/UseEffect.test.js b/scripts/__tests__/ComponentTest/UseEffect.test.js index 0eccd5f9..1a0631fe 100644 --- a/scripts/__tests__/ComponentTest/UseEffect.test.js +++ b/scripts/__tests__/ComponentTest/UseEffect.test.js @@ -4,7 +4,7 @@ import * as LogUtils from '../jest/logUtils'; import { act } from '../jest/customMatcher'; describe('useEffect Hook Test', () => { - const { useEffect, useLayoutEffect, useState, memo } = React; + const { useEffect, useLayoutEffect, useState, memo, forwardRef, useImperativeHandle } = React; const { unmountComponentAtNode } = HorizonDOM; let container = null; beforeEach(() => { @@ -27,6 +27,30 @@ describe('useEffect Hook Test', () => { return

{props.text}

; } + it('act方法', () => { + const App = () => { + return ; + } + + act(() => { + HorizonDOM.render(, container, () => { + LogUtils.log('num effect'); + }); + // 第一次渲染为同步,所以同步执行的可以写在act里做判断 + expect(LogUtils.getAndClear()).toEqual(['op', 'num effect']); + expect(container.textContent).toBe('op'); + }); + act(() => { + HorizonDOM.render(null, container, () => { + LogUtils.log('num effect89'); + }); + // 第二次渲染为异步,所以同步执行的不可以写在act里做判断,act里拿到的为空数组 + expect(LogUtils.getAndClear()).toEqual([]); + }); + expect(LogUtils.getAndClear()).toEqual(['num effect89']); + expect(container.textContent).toBe(''); + }); + it('兄弟节点被删除,useEffect依然正常', () => { const App = () => { return ; @@ -123,13 +147,19 @@ describe('useEffect Hook Test', () => { 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]']); + expect(LogUtils.getAndClear()).toEqual(['First effect [0]']); + act(() => { + HorizonDOM.render(, container, () => LogUtils.log('callback effect')); + + }) + // 此时异步执行,act执行完后会执行新render的useEffect + expect(LogUtils.getAndClear()).toEqual([ + 'num: 1', + 'callback effect', + 'First effect [1]' + ]); + expect(container.textContent).toEqual('num: 1'); }); it('混合使用useEffect', () => { @@ -150,10 +180,16 @@ describe('useEffect Hook Test', () => { 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]']); + // 第二次render时异步执行,act保证所有效果都已更新,所以先常规记录日志 + // 然后记录useEffect的日志 + expect(LogUtils.getAndClear()).toEqual([ + 'num: 1', + 'callback effect', + 'First effect [1]', + 'Second effect [1]' + ]); + expect(container.textContent).toEqual('num: 1'); }); it('创建,销毁useEffect', () => { @@ -202,14 +238,13 @@ describe('useEffect Hook Test', () => { 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([ + 'num: 0,word: React', + 'word Layouteffect destroy', + 'word Layouteffect [React]', + 'callback effect', + // 最后执行异步的 'word effect destroy', 'word effect [React]', ]); @@ -217,13 +252,12 @@ describe('useEffect Hook Test', () => { 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 Layouteffect destroy', + 'word Layouteffect destroy', + 'callback effect', + // 最后执行异步useEffect 'num effect destroy', 'word effect destroy', ]); @@ -254,27 +288,26 @@ describe('useEffect Hook Test', () => { 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: 1', + 'callback effect', + // 最后执行异步 'num effect destroy', 'num effect [1]', ]); + expect(container.textContent).toEqual('num: 1'); + 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([ + 'callback effect', 'num effect destroy' ]); + expect(container.textContent).toEqual(''); + expect(LogUtils.getAndClear()).toEqual([]); }); it('销毁依赖空数组的useEffect', () => { @@ -302,25 +335,24 @@ describe('useEffect Hook Test', () => { 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([ + 'num: 1', + 'callback effect' + // 依赖空数组,没有执行useEffect + ]); + expect(container.textContent).toEqual('num: 1'); 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([ + 'callback effect', 'num effect destroy' ]); + expect(container.textContent).toEqual(''); + expect(LogUtils.getAndClear()).toEqual([]); }); it('useEffect里使用useState(1', () => { @@ -354,11 +386,12 @@ describe('useEffect Hook Test', () => { act(() => { setNum(); - expect(LogUtils.getAndClear()).toEqual([ - 'num: 1' - ]); }); - expect(LogUtils.getAndClear()).toEqual(['num effect [1]']); + expect(LogUtils.getAndClear()).toEqual([ + 'num: 1', + 'num effect [1]' + ]); + expect(LogUtils.getAndClear()).toEqual([]); }); it('useEffect里使用useState(2', () => { @@ -389,7 +422,7 @@ describe('useEffect Hook Test', () => { expect(container.textContent).toEqual('Num: 1'); }); - it('useEffect与memo一起使用', () => { + it('useEffect与memo一起使用(1', () => { let setNum; const App = memo(() => { const [num, _setNum] = useState(0); @@ -415,28 +448,284 @@ describe('useEffect Hook Test', () => { // 不会重新渲染 act(() => { HorizonDOM.render(, container, () => LogUtils.log('callback effect')); - expect(LogUtils.getAndClear()).toEqual(['callback effect']); - expect(container.textContent).toEqual('0'); }); + 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([ + 1, 'num effect destroy 0', 'num effect [1]' ]); + expect(container.textContent).toEqual('1'); + 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 1']); + expect(LogUtils.getAndClear()).toEqual([ + 'callback effect', + 'num effect destroy 1' + ]); + expect(container.textContent).toEqual(''); + expect(LogUtils.getAndClear()).toEqual([]); }); + it('useEffect与memo一起使用(2', () => { + const compare = (prevProps, nextProps) => prevProps.num === nextProps.num; + const App = memo((props) => { + useEffect(() => { + LogUtils.log(`num effect [${props.num}]`); + return () => { + LogUtils.log(`num effect destroy ${props.num}`); + }; + }); + return ; + }, compare) + 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(() => { + HorizonDOM.render(, container, () => LogUtils.log('callback effect')); + }); + expect(LogUtils.getAndClear()).toEqual([ + 1, + 'callback effect', + // 执行异步,先清除旧的,再执行新的 + 'num effect destroy 0', + 'num effect [1]' + ]); + expect(container.textContent).toEqual('1'); + expect(LogUtils.getAndClear()).toEqual([]); + + act(() => { + HorizonDOM.render(null, container, () => LogUtils.log('callback effect')); + }); + expect(LogUtils.getAndClear()).toEqual(['callback effect', 'num effect destroy 1']); + expect(container.textContent).toEqual(''); + expect(LogUtils.getAndClear()).toEqual([]); + }); + + it('useEffect处理错误', () => { + const App = (props) => { + useEffect(() => { + LogUtils.log('throw Error'); + throw new Error('mistake'); + // eslint-disable-next-line no-unreachable + LogUtils.log(`Mount with [${props.num}]`); + return () => { + LogUtils.log(`Unmount with [${props.num}]`); + }; + }); + return ; + } + act(() => { + HorizonDOM.render(, container, () => + LogUtils.log('Sync effect'), + ); + expect(LogUtils.getAndClear()).toEqual(['Number: 0', 'Sync effect']); + expect(container.textContent).toEqual('Number: 0'); + }); + // 处理错误,不会向下执行LogUtils.log(`Mount with [${props.num}]`); + expect(LogUtils.getAndClear()).toEqual(['throw Error']); + + act(() => { + HorizonDOM.render(null, container, () => + LogUtils.log('Sync effect'), + ); + }); + expect(LogUtils.getAndClear()).toEqual([ + 'Sync effect', + // 不会处理卸载部分 LogUtils.log(`Unmount with [${props.num}]`); + ]); + expect(container.textContent).toEqual(''); + expect(LogUtils.getAndClear()).toEqual([]); + }); + + it('卸载useEffect', () => { + const App = (props) => { + useEffect(() => { + LogUtils.log(`num effect [${props.num}]`); + return () => { + LogUtils.log(`num effect destroy ${props.num}`); + }; + }, []); + if (props.num < 0) { + useEffect(() => { + LogUtils.log(`New num effect [${props.num}]`); + return () => { + LogUtils.log(`New num effect destroy ${props.num}`); + }; + }, []); + } + return ; + } + + act(() => { + HorizonDOM.render(, container, () => LogUtils.log('num effect')); + expect(LogUtils.getAndClear()).toEqual(['Number: 0', 'num effect']); + expect(container.textContent).toBe('Number: 0'); + }); + expect(LogUtils.getAndClear()).toEqual(['num effect [0]']); + + act(() => { + HorizonDOM.render(null, container, () => LogUtils.log('num effect')); + }); + expect(LogUtils.getAndClear()).toEqual(['num effect', 'num effect destroy 0']); + expect(container.textContent).toBe(''); + expect(LogUtils.getAndClear()).toEqual([]); + }); + + it('同步刷新不会导致effect执行', () => { + let setNum; + const App = () => { + const [num, _setNum] = useState(0); + setNum = _setNum; + useEffect(() => { + LogUtils.log(`num effect [${num}]`); + _setNum(1); + }, []); + return ; + } + + HorizonDOM.render(, container, () => LogUtils.log('num effect')); + expect(LogUtils.getAndClear()).toEqual(['Number: 0', 'num effect']); + expect(container.textContent).toBe('Number: 0'); + + act(() => { + // 模拟同步刷新 + (function () { + setNum(2); + })(); + }); + expect(LogUtils.getAndClear()).toEqual(['num effect [0]', 'Number: 1']); + expect(container.textContent).toBe('Number: 1'); + expect(LogUtils.getAndClear()).toEqual([]); + }); + + it('当组件的更新方法在卸载函数中,组件更新不会告警', () => { + const App = () => { + LogUtils.log('useEffect'); + const [num, setNum] = useState(0); + useEffect(() => { + LogUtils.log('effect'); + return () => { + setNum(1); + LogUtils.log('effect destroy'); + }; + }, []); + return num; + } + + act(() => { + HorizonDOM.render(, container, () => LogUtils.log('num effect')); + expect(LogUtils.getAndClear()).toEqual(['useEffect', 'num effect']); + }); + expect(LogUtils.getAndClear()).toEqual(['effect']); + + act(() => { + HorizonDOM.render(null, container); + }); + // 不会处理setNum(1) + expect(LogUtils.getAndClear()).toEqual(['effect destroy']); + }); + + it('当组件的更新方法在卸载函数中,组件的子组件更新不会告警', () => { +const App = () => { + LogUtils.log('App'); + const appRef = React.createRef(null); + useEffect(() => { + LogUtils.log('App effect'); + return () => { + appRef.current(1); + LogUtils.log('App effect destroy'); + }; + }, []); + return ; + } + + let AppChild = (props, ref) => { + LogUtils.log('AppChild') + const [num, setNum] = useState(0); + useEffect(() => { + LogUtils.log('Child effect'); + ref.current = setNum; + }, []); + return num; + } + AppChild = forwardRef(AppChild); + + act(() => { + HorizonDOM.render(, container, () => LogUtils.log('num effect')); + expect(LogUtils.getAndClear()).toEqual([ + 'App', + 'AppChild', + 'num effect' + ]); + }); + expect(LogUtils.getAndClear()).toEqual(['Child effect','App effect']); + + act(() => { + HorizonDOM.render(null, container); + }); + // 销毁时执行appRef.current(1)不会报错 + expect(LogUtils.getAndClear()).toEqual(['App effect destroy']); + }); + + it('当组件的更新方法在卸载函数中,组件的父组件更新不会告警', () => { + const App = () => { + LogUtils.log('App'); + const [num, setNum] = useState(0); + return ; + } + + let AppChild = (props) => { + LogUtils.log('AppChild') + useEffect(() => { + LogUtils.log('Child effect'); + return () => { + LogUtils.log('Child effect destroy'); + props.setNum(1); + }; + }, []); + return props.num; + } + + act(() => { + HorizonDOM.render(, container, () => LogUtils.log('num effect')); + expect(LogUtils.getAndClear()).toEqual([ + 'App', + 'AppChild', + 'num effect' + ]); + }); + expect(LogUtils.getAndClear()).toEqual(['Child effect']); + + act(() => { + HorizonDOM.render(null, container); + }); + // 销毁时执行 props.setNum(1);不会报错 + expect(LogUtils.getAndClear()).toEqual(['Child effect destroy']); + }); }) diff --git a/scripts/__tests__/ComponentTest/UseState.test.js b/scripts/__tests__/ComponentTest/UseState.test.js index ffd1cda0..02c3996f 100644 --- a/scripts/__tests__/ComponentTest/UseState.test.js +++ b/scripts/__tests__/ComponentTest/UseState.test.js @@ -1,6 +1,7 @@ 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('useState Hook Test', () => { const { useState, forwardRef, useImperativeHandle, memo } = React; @@ -126,4 +127,45 @@ describe('useState Hook Test', () => { expect(LogUtils.getAndClear()).toEqual([1]); expect(container.querySelector('p').innerHTML).toBe('1'); }); + + it('卸载useState', () => { + let updateA; + let updateB; + let updateC; + + const App = (props) => { + const [A, _updateA] = useState(0); + const [B, _updateB] = useState(0); + updateA = _updateA; + updateB = _updateB; + + let C; + if (props.loadC) { + const [_C, _updateC] = useState(0); + C = _C; + updateC = _updateC; + } else { + C = '[not loaded]'; + } + + return ; + } + + HorizonDOM.render(, container); + expect(LogUtils.getAndClear()).toEqual(['A: 0, B: 0, C: 0']); + expect(container.textContent).toBe('A: 0, B: 0, C: 0'); + act(() => { + updateA(2); + updateB(3); + updateC(4); + }); + expect(LogUtils.getAndClear()).toEqual(['A: 2, B: 3, C: 4']); + expect(container.textContent).toBe('A: 2, B: 3, C: 4'); + + expect(() => { + HorizonDOM.render(, container); + }).toThrow( + 'Hooks are less than expected, please check whether the hook is written in the condition.', + ); + }); }); diff --git a/scripts/__tests__/jest/customMatcher.js b/scripts/__tests__/jest/customMatcher.js index f921e916..c373fbd5 100644 --- a/scripts/__tests__/jest/customMatcher.js +++ b/scripts/__tests__/jest/customMatcher.js @@ -1,5 +1,7 @@ -import { runAsyncEffects } from '../../../libs/horizon/src/renderer/submit/HookEffectHandler'; +import { runAsyncEffects, callUseEffects } from '../../../libs/horizon/src/renderer/submit/HookEffectHandler'; import { callRenderQueueImmediate } from '../../../libs/horizon/src/renderer/taskExecutor/RenderQueue'; +import { asyncUpdates } from '../../../libs/horizon/src/renderer/TreeBuilder'; +import { runAsync } from '../../../libs/horizon/src/renderer/taskExecutor/TaskExecutor'; function runAssertion(fn) { try { @@ -21,7 +23,7 @@ function toMatchValue(LogUtils, expectedValues) { } const act = (fun) => { - fun(); + asyncUpdates(fun); callRenderQueueImmediate(); runAsyncEffects(); } From 4d2cd8276ac95fa3de4c3172d19e59e8fe47e68b Mon Sep 17 00:00:00 2001 From: * <8> Date: Sun, 13 Mar 2022 13:13:06 +0800 Subject: [PATCH 3/5] Match-id-38f76aafad7b269e2beed396a0ef1c14487adfc3 --- scripts/__tests__/ComponentTest/UseEffect.test.js | 2 +- scripts/__tests__/jest/customMatcher.js | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/scripts/__tests__/ComponentTest/UseEffect.test.js b/scripts/__tests__/ComponentTest/UseEffect.test.js index 1a0631fe..89513844 100644 --- a/scripts/__tests__/ComponentTest/UseEffect.test.js +++ b/scripts/__tests__/ComponentTest/UseEffect.test.js @@ -4,7 +4,7 @@ import * as LogUtils from '../jest/logUtils'; import { act } from '../jest/customMatcher'; describe('useEffect Hook Test', () => { - const { useEffect, useLayoutEffect, useState, memo, forwardRef, useImperativeHandle } = React; + const { useEffect, useLayoutEffect, useState, memo, forwardRef } = React; const { unmountComponentAtNode } = HorizonDOM; let container = null; beforeEach(() => { diff --git a/scripts/__tests__/jest/customMatcher.js b/scripts/__tests__/jest/customMatcher.js index c373fbd5..1f6a96fb 100644 --- a/scripts/__tests__/jest/customMatcher.js +++ b/scripts/__tests__/jest/customMatcher.js @@ -1,7 +1,6 @@ -import { runAsyncEffects, callUseEffects } from '../../../libs/horizon/src/renderer/submit/HookEffectHandler'; +import { runAsyncEffects } from '../../../libs/horizon/src/renderer/submit/HookEffectHandler'; import { callRenderQueueImmediate } from '../../../libs/horizon/src/renderer/taskExecutor/RenderQueue'; import { asyncUpdates } from '../../../libs/horizon/src/renderer/TreeBuilder'; -import { runAsync } from '../../../libs/horizon/src/renderer/taskExecutor/TaskExecutor'; function runAssertion(fn) { try { From 2100ebcb5731eb99670a13d247cc42302c15e737 Mon Sep 17 00:00:00 2001 From: * <8> Date: Tue, 15 Mar 2022 17:42:45 +0800 Subject: [PATCH 4/5] Match-id-991d5f810c56de98d317600d544e7e30fd874144 --- jest.config.js | 18 +- .../HookTest/UseCallback.test.js | 36 ++ .../ComponentTest/HookTest/UseContext.test.js | 52 +++ .../{ => HookTest}/UseEffect.test.js | 69 ++-- .../HookTest/UseImperativeHandle.test.js | 89 +++++ .../HookTest/UseLayoutEffect.test.js | 116 +++++++ .../ComponentTest/HookTest/UseMemo.test.js | 107 ++++++ .../ComponentTest/HookTest/UseReducer.test.js | 61 ++++ .../ComponentTest/HookTest/UseRef.test.js | 58 ++++ .../{ => HookTest}/UseState.test.js | 93 +++--- .../HooksExceptEffectAndState.test.js | 249 -------------- .../ComponentTest/SimpleUseHook.test.js | 312 ------------------ scripts/__tests__/jest/Text.js | 9 + scripts/__tests__/jest/jestSetting.js | 24 +- 14 files changed, 638 insertions(+), 655 deletions(-) create mode 100644 scripts/__tests__/ComponentTest/HookTest/UseCallback.test.js create mode 100644 scripts/__tests__/ComponentTest/HookTest/UseContext.test.js rename scripts/__tests__/ComponentTest/{ => HookTest}/UseEffect.test.js (93%) create mode 100644 scripts/__tests__/ComponentTest/HookTest/UseImperativeHandle.test.js create mode 100644 scripts/__tests__/ComponentTest/HookTest/UseLayoutEffect.test.js create mode 100644 scripts/__tests__/ComponentTest/HookTest/UseMemo.test.js create mode 100644 scripts/__tests__/ComponentTest/HookTest/UseReducer.test.js create mode 100644 scripts/__tests__/ComponentTest/HookTest/UseRef.test.js rename scripts/__tests__/ComponentTest/{ => HookTest}/UseState.test.js (68%) delete mode 100644 scripts/__tests__/ComponentTest/HooksExceptEffectAndState.test.js delete mode 100644 scripts/__tests__/ComponentTest/SimpleUseHook.test.js create mode 100644 scripts/__tests__/jest/Text.js diff --git a/jest.config.js b/jest.config.js index 333fc68e..1c30d15d 100644 --- a/jest.config.js +++ b/jest.config.js @@ -58,15 +58,15 @@ module.exports = { // globalTeardown: undefined, // A set of global variables that need to be available in all test environments - // globals: { - // 'isDev': process.env.NODE_ENV === 'development', - // 'MessageChannel': function MessageChannel() { - // this.port1 = {}; - // this.port2 = { - // postMessage() {} - // }; - // } - // }, + globals: { + //'isDev': process.env.NODE_ENV === 'development', + 'MessageChannel': function MessageChannel() { + this.port1 = {}; + this.port2 = { + postMessage() {} + }; + } + }, // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. // maxWorkers: "50%", diff --git a/scripts/__tests__/ComponentTest/HookTest/UseCallback.test.js b/scripts/__tests__/ComponentTest/HookTest/UseCallback.test.js new file mode 100644 index 00000000..071c4512 --- /dev/null +++ b/scripts/__tests__/ComponentTest/HookTest/UseCallback.test.js @@ -0,0 +1,36 @@ +/* eslint-disable no-undef */ +import * as React from '../../../../libs/horizon/src/external/Horizon'; +import * as HorizonDOM from '../../../../libs/horizon/src/dom/DOMExternal'; + +describe('useCallback Hook Test', () => { + const { useState, useCallback } = React; + + it('测试useCallback', () => { + const App = (props) => { + const [num, setNum] = useState(0); + const NumUseCallback = useCallback(() => { + setNum(num + props.text) + }, [props]); + return ( + <> +

{num}

+ + ; + + ) + + } + HorizonDOM.render(, container); + expect(LogUtils.getAndClear()).toEqual([1]); + expect(container.querySelector('p').innerHTML).toBe('1'); + // 点击按钮触发ref.current加1 + container.querySelector('button').click(); + // ref.current改变不会触发重新渲染 + expect(LogUtils.getAndClear()).toEqual([]); + expect(container.querySelector('p').innerHTML).toBe('1'); + }); +}); diff --git a/scripts/__tests__/ComponentTest/UseState.test.js b/scripts/__tests__/ComponentTest/HookTest/UseState.test.js similarity index 68% rename from scripts/__tests__/ComponentTest/UseState.test.js rename to scripts/__tests__/ComponentTest/HookTest/UseState.test.js index 02c3996f..adccf421 100644 --- a/scripts/__tests__/ComponentTest/UseState.test.js +++ b/scripts/__tests__/ComponentTest/HookTest/UseState.test.js @@ -1,31 +1,35 @@ -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'; +/* eslint-disable no-undef */ +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'; +import Text from '../../jest/Text'; describe('useState Hook Test', () => { - const { useState, forwardRef, useImperativeHandle, memo } = React; - const { unmountComponentAtNode } = HorizonDOM; - let container = null; - beforeEach(() => { - // 创建一个 DOM 元素作为渲染目标 - container = document.createElement('div'); - document.body.appendChild(container); - }); + const { + useState, + forwardRef, + useImperativeHandle, + memo + } = React; - afterEach(() => { - // 退出时进行清理 - unmountComponentAtNode(container); - container.remove(); - container = null; - LogUtils.clear(); + it('简单使用useState', () => { + const App = () => { + const [num, setNum] = useState(0); + return ( + <> +

{num}

+ - ; - - ) - - } - HorizonDOM.render(, container); - expect(LogUtils.getAndClear()).toEqual([1]); - expect(container.querySelector('p').innerHTML).toBe('1'); - // 点击按钮触发ref.current加1 - container.querySelector('button').click(); - // ref.current改变不会触发重新渲染 - expect(LogUtils.getAndClear()).toEqual([]); - expect(container.querySelector('p').innerHTML).toBe('1'); - }); - }) - - describe('useImperativeHandle Test', () => { - it('useImperativeHandle没有配置dep时自动更新', () => { - - let App = (props, ref) => { - const [num, setNum] = useState(0); - useImperativeHandle(ref, () => ({ num, setNum })); - return ; - } - let App1 = (props, ref) => { - const [num1, setNum1] = useState(0); - useImperativeHandle(ref, () => ({ num1, setNum1 }), []); - return ; - } - - App = forwardRef(App); - App1 = forwardRef(App1); - const counter = React.createRef(null); - const counter1 = React.createRef(null); - HorizonDOM.render(, container); - expect(LogUtils.getAndClear()).toEqual([0]); - expect(counter.current.num).toBe(0); - act(() => { - counter.current.setNum(1); - }); - expect(LogUtils.getAndClear()).toEqual([1]); - // useImperativeHandle没有配置的dep,所以会自动更新 - expect(counter.current.num).toBe(1); - // 清空container - unmountComponentAtNode(container); - - HorizonDOM.render(, container); - expect(LogUtils.getAndClear()).toEqual([0]); - expect(counter1.current.num1).toBe(0); - act(() => { - counter1.current.setNum1(1); - }); - expect(LogUtils.getAndClear()).toEqual([1]); - // useImperativeHandle的dep为[],所以不会变 - expect(counter1.current.num1).toBe(0); - }); - }) -}) diff --git a/scripts/__tests__/ComponentTest/SimpleUseHook.test.js b/scripts/__tests__/ComponentTest/SimpleUseHook.test.js deleted file mode 100644 index 0d99f8ed..00000000 --- a/scripts/__tests__/ComponentTest/SimpleUseHook.test.js +++ /dev/null @@ -1,312 +0,0 @@ -import * as React from '../../../libs/horizon/src/external/Horizon'; -import * as HorizonDOM from '../../../libs/horizon/src/dom/DOMExternal'; -import { act } from '../jest/customMatcher'; - - -describe('Hook Test', () => { - const { - useState, - useReducer, - useEffect, - useLayoutEffect, - useContext, - useMemo, - useCallback, - useRef, - useImperativeHandle, - forwardRef - } = React; - const { unmountComponentAtNode } = HorizonDOM; - let container = null; - beforeEach(() => { - // 创建一个 DOM 元素作为渲染目标 - container = document.createElement('div'); - document.body.appendChild(container); - }); - - afterEach(() => { - // 退出时进行清理 - unmountComponentAtNode(container); - container.remove(); - container = null; - }); - - - it('简单使用useState', () => { - const App = () => { - const [num, setNum] = useState(0); - return ( - <> -

{num}

-