diff --git a/scripts/__tests__/ComponentTest/ComponentError.test.js b/scripts/__tests__/ComponentTest/ComponentError.test.js new file mode 100755 index 00000000..7a833d04 --- /dev/null +++ b/scripts/__tests__/ComponentTest/ComponentError.test.js @@ -0,0 +1,43 @@ +import * as Horizon from '@cloudsop/horizon/index.ts'; +import { getLogUtils } from '../jest/testUtils'; + +describe('Component Error Test', () => { + const LogUtils = getLogUtils(); + it('createElement不能为null或undefined', () => { + const NullElement = null; + const UndefinedElement = undefined; + + jest.spyOn(console, 'error').mockImplementation(); + expect(() => { + Horizon.render(, document.createElement('div')); + }).toThrow('Component type is invalid, got: null'); + + expect(() => { + Horizon.render(, document.createElement('div')); + }).toThrow('Component type is invalid, got: undefined'); + + const App = () => { + return ; + }; + + let AppChild = () => { + return ( + + ); + }; + + expect(() => { + Horizon.render(, document.createElement('div')); + }).toThrow('Component type is invalid, got: null'); + + AppChild = () => { + return ( + + ); + }; + + expect(() => { + Horizon.render(, document.createElement('div')); + }).toThrow('Component type is invalid, got: undefined'); + }); +}); \ No newline at end of file diff --git a/scripts/__tests__/ComponentTest/FragmentComponent.test.js b/scripts/__tests__/ComponentTest/FragmentComponent.test.js new file mode 100755 index 00000000..eee9ceb4 --- /dev/null +++ b/scripts/__tests__/ComponentTest/FragmentComponent.test.js @@ -0,0 +1,474 @@ +import * as Horizon from '@cloudsop/horizon/index.ts'; +import { Text } from '../jest/commonComponents'; +import { getLogUtils } from '../jest/testUtils'; + +describe('Fragment', () => { + const LogUtils = getLogUtils(); + const { + useEffect, + useRef, + act, + } = Horizon; + it('可以渲染空元素', () => { + const element = ( + + ); + + Horizon.render(element, container); + + expect(container.textContent).toBe(''); + }); + it('可以渲染单个元素', () => { + const element = ( + + + + ); + + Horizon.render(element, container); + + expect(LogUtils.getAndClear()).toEqual(['Fragment']); + expect(container.textContent).toBe('Fragment'); + }); + + it('可以渲染混合元素', () => { + const element = ( + + Java and + + ); + + Horizon.render(element, container); + + expect(LogUtils.getAndClear()).toEqual(['JavaScript']); + expect(container.textContent).toBe('Java and JavaScript'); + }); + + it('可以渲染集合元素', () => { + const App = [, ]; + const element = ( + <> + {App} + + ); + + Horizon.render(element, container); + + expect(LogUtils.getAndClear()).toEqual(['Java', 'JavaScript']); + expect(container.textContent).toBe('JavaJavaScript'); + }); + + it('元素被放进不同层级Fragment里时,状态不会保留', () => { + const ChildApp = (props) => { + const flag = useRef(true); + useEffect(() => { + if (flag.current) { + flag.current = false; + } else { + LogUtils.log('useEffect'); + } + }); + + return

{props.logo}

; + }; + + const App = (props) => { + return props.change ? ( + <> + + + + ) : ( + <> + <> + + + + ); + }; + + act(() => { + Horizon.render(, container); + }); + expect(LogUtils.getNotClear()).toEqual([]); + act(() => { + Horizon.render(, container); + }); + // 切换到不同层级Fragment时,副作用状态不会保留 + expect(LogUtils.getNotClear()).toEqual([]); + expect(container.textContent).toBe('2'); + + act(() => { + Horizon.render(, container); + }); + expect(LogUtils.getNotClear()).toEqual([]); + expect(container.textContent).toBe('1'); + }); + + it('元素被放进单层Fragment里,且在Fragment的顶部时,状态会保留', () => { + const ChildApp = (props) => { + const flag = useRef(true); + useEffect(() => { + if (flag.current) { + flag.current = false; + } else { + LogUtils.log('useEffect'); + } + }); + + return

{props.logo}

; + }; + + const App = (props) => { + return props.change ? ( + + ) : ( + <> + + + ); + }; + + act(() => { + Horizon.render(, container); + }); + expect(LogUtils.getNotClear()).toEqual([]); + act(() => { + Horizon.render(, container); + }); + // 状态会保留 + expect(LogUtils.getNotClear()).toEqual(['useEffect']); + expect(container.textContent).toBe('2'); + + act(() => { + Horizon.render(, container); + }); + expect(LogUtils.getNotClear()).toEqual(['useEffect', 'useEffect']); + expect(container.textContent).toBe('1'); + }); + + it('元素被放进单层Fragment里,但不在Fragment的顶部时,状态不会保留', () => { + const ChildApp = (props) => { + const flag = useRef(true); + useEffect(() => { + if (flag.current) { + flag.current = false; + } else { + LogUtils.log('useEffect'); + } + }); + + return

{props.logo}

; + }; + + const App = (props) => { + return props.change ? ( + + ) : ( + <> +
123
+ + + ); + }; + + act(() => { + Horizon.render(, container); + }); + expect(LogUtils.getNotClear()).toEqual([]); + act(() => { + Horizon.render(, container); + }); + // 状态不会保留 + expect(LogUtils.getNotClear()).toEqual([]); + expect(container.textContent).toBe('1232'); + + act(() => { + Horizon.render(, container); + }); + expect(LogUtils.getNotClear()).toEqual([]); + expect(container.textContent).toBe('1'); + }); + + it('元素被放进多层Fragment里时,状态不会保留', () => { + const ChildApp = (props) => { + const flag = useRef(true); + useEffect(() => { + if (flag.current) { + flag.current = false; + } else { + LogUtils.log('useEffect'); + } + }); + + return

{props.logo}

; + }; + + const App = (props) => { + return props.change ? ( + + ) : ( + <> + <> + <> + + + + + ); + }; + + act(() => { + Horizon.render(, container); + }); + expect(LogUtils.getNotClear()).toEqual([]); + act(() => { + Horizon.render(, container); + }); + // 状态不会保留 + expect(LogUtils.getNotClear()).toEqual([]); + expect(container.textContent).toBe('2'); + + act(() => { + Horizon.render(, container); + }); + expect(LogUtils.getNotClear()).toEqual([]); + expect(container.textContent).toBe('1'); + }); + + it('元素被切换放进同级Fragment里时,状态会保留', () => { + const ChildApp = (props) => { + const flag = useRef(true); + useEffect(() => { + if (flag.current) { + flag.current = false; + } else { + LogUtils.log('useEffect'); + } + }); + + return

{props.logo}

; + }; + + const App = (props) => { + return props.change ? ( + <> + <> + <> + + + + + ) : ( + <> + <> + <> + + + + + ); + }; + + act(() => { + Horizon.render(, container); + }); + expect(LogUtils.getNotClear()).toEqual([]); + act(() => { + Horizon.render(, container); + }); + // 状态会保留 + expect(LogUtils.getNotClear()).toEqual(['useEffect']); + expect(container.textContent).toBe('2'); + + act(() => { + Horizon.render(, container); + }); + expect(LogUtils.getNotClear()).toEqual(['useEffect', 'useEffect']); + expect(container.textContent).toBe('1'); + }); + + it('元素被切换放进同级Fragment,且在数组顶层时,状态会保留', () => { + const ChildApp = (props) => { + const flag = useRef(true); + useEffect(() => { + if (flag.current) { + flag.current = false; + } else { + LogUtils.log('useEffect'); + } + }); + + return

{props.logo}

; + }; + + const App = (props) => { + return props.change ? ( + <> + <> + <> + + + + + ) : ( + <> + <> + <> + {[]} + + + + ); + }; + + act(() => { + Horizon.render(, container); + }); + expect(LogUtils.getNotClear()).toEqual([]); + act(() => { + Horizon.render(, container); + }); + // 状态会保留 + expect(LogUtils.getNotClear()).toEqual(['useEffect']); + expect(container.textContent).toBe('2'); + + act(() => { + Horizon.render(, container); + }); + expect(LogUtils.getNotClear()).toEqual(['useEffect', 'useEffect']); + expect(container.textContent).toBe('1'); + }); + + it('数组里的顶层元素被切换放进单级Fragment时,状态会保留', () => { + const ChildApp = (props) => { + const flag = useRef(true); + useEffect(() => { + if (flag.current) { + flag.current = false; + } else { + LogUtils.log('useEffect'); + } + }); + + return

{props.logo}

; + }; + + const App = (props) => { + return props.change ? ( + [] + ) : ( + <> + + + ); + }; + + act(() => { + Horizon.render(, container); + }); + expect(LogUtils.getNotClear()).toEqual([]); + act(() => { + Horizon.render(, container); + }); + // 状态会保留 + expect(LogUtils.getNotClear()).toEqual(['useEffect']); + expect(container.textContent).toBe('2'); + + act(() => { + Horizon.render(, container); + }); + expect(LogUtils.getNotClear()).toEqual(['useEffect', 'useEffect']); + expect(container.textContent).toBe('1'); + }); + + it('Fragment里的顶层数组里的顶层元素被切换放进不同级Fragment时,状态不会保留', () => { + const ChildApp = (props) => { + const flag = useRef(true); + useEffect(() => { + if (flag.current) { + flag.current = false; + } else { + LogUtils.log('useEffect'); + } + }); + + return

{props.logo}

; + }; + + const App = (props) => { + return props.change ? ( + <> + [] + + ) : ( + <> + <> + + + + ); + }; + + act(() => { + Horizon.render(, container); + }); + expect(LogUtils.getNotClear()).toEqual([]); + act(() => { + Horizon.render(, container); + }); + // 状态会保留 + expect(LogUtils.getNotClear()).toEqual([]); + expect(container.textContent).toBe('2'); + + act(() => { + Horizon.render(, container); + }); + expect(LogUtils.getNotClear()).toEqual([]); + expect(container.textContent).toBe('[1]'); + }); + + it('Fragment的key值不同时,状态不会保留', () => { + const ChildApp = (props) => { + const flag = useRef(true); + useEffect(() => { + if (flag.current) { + flag.current = false; + } else { + LogUtils.log('useEffect'); + } + }); + + return

{props.logo}

; + }; + + const App = (props) => { + return props.change ? ( + + + + ) : ( + + + + ); + }; + + act(() => { + Horizon.render(, container); + }); + expect(LogUtils.getNotClear()).toEqual([]); + act(() => { + Horizon.render(, container); + }); + // 状态不会保留 + expect(LogUtils.getNotClear()).toEqual([]); + expect(container.textContent).toBe('2'); + + act(() => { + Horizon.render(, container); + }); + expect(LogUtils.getNotClear()).toEqual([]); + expect(container.textContent).toBe('1'); + }); +}); diff --git a/scripts/__tests__/ComponentTest/LazyComponent.test.js b/scripts/__tests__/ComponentTest/LazyComponent.test.js new file mode 100755 index 00000000..704dca45 --- /dev/null +++ b/scripts/__tests__/ComponentTest/LazyComponent.test.js @@ -0,0 +1,187 @@ +import * as Horizon from '@cloudsop/horizon/index.ts'; +import { Text } from '../jest/commonComponents'; +import { getLogUtils } from '../jest/testUtils'; + +describe('LazyComponent Test', () => { + const LogUtils = getLogUtils(); + const mockImport = jest.fn(async (component) => { + return { default: component }; + }); + + it('Horizon.lazy()', async () => { + class LazyComponent extends Horizon.Component { + static defaultProps = { language: 'Java' }; + + render() { + const text = `${this.props.greeting}: ${this.props.language}`; + return {text}; + } + } + + const Lazy = Horizon.lazy(() => mockImport(LazyComponent)); + + Horizon.render( + }> + + , + container + ); + + expect(LogUtils.getAndClear()).toEqual(['Loading...']); + expect(container.textContent).toBe('Loading...'); + expect(container.querySelector('span')).toBe(null); + + await Promise.resolve(); + Horizon.render( + }> + + , + container + ); + + expect(LogUtils.getAndClear()).toEqual([]); + expect(container.querySelector('span').innerHTML).toBe('Goodbye: Java'); + }); + + it('同步解析', async () => { + const LazyApp = Horizon.lazy(() => ({ + then(cb) { + cb({ default: Text }); + }, + })); + + Horizon.render( + Loading...}> + + , + container + ); + + expect(LogUtils.getAndClear()).toEqual(['Lazy']); + expect(container.textContent).toBe('Lazy'); + }); + + it('异常捕获边界', async () => { + class ErrorBoundary extends Horizon.Component { + state = {}; + static getDerivedStateFromError(error) { + return { message: error.message }; + } + render() { + return this.state.message + ?

Error: {this.state.message}

+ : this.props.children; + } + } + + const LazyComponent = () => { + const [num, setNum] = Horizon.useState(0); + if (num === 2) { + throw new Error('num is 2'); + } else { + return ( + <> +

{num}

+ + + ); + }; + + + const App = () => { + const handleClick = () => { + LogUtils.log('bubble click event'); + }; + + const handleCaptureClick = () => { + LogUtils.log('capture click event'); + }; + + return ( +
+ }> + + +
+ ); + }; + Horizon.render(, container); + const event = document.createEvent('Event'); + event.initEvent('click', true, true); + buttonRef.current.dispatchEvent(event); + + expect(LogUtils.getAndClear()).toEqual([ + // 从外到内先捕获再冒泡 + 'capture click event', + 'bubble click event' + ]); + }); +}); \ No newline at end of file diff --git a/scripts/__tests__/ComponentTest/SuspenseComponent.test.js b/scripts/__tests__/ComponentTest/SuspenseComponent.test.js new file mode 100755 index 00000000..4409dd9c --- /dev/null +++ b/scripts/__tests__/ComponentTest/SuspenseComponent.test.js @@ -0,0 +1,59 @@ +import * as Horizon from '@cloudsop/horizon/index.ts'; +import { Text } from '../jest/commonComponents'; +import { getLogUtils } from '../jest/testUtils'; + +describe('SuspenseComponent Test', () => { + const LogUtils = getLogUtils(); + const mockImport = jest.fn(async (component) => { + return { default: component }; + }); + + // var EMPTY_OBJECT = {}; + // const mockCreateResource = jest.fn((component) => { + // let result = EMPTY_OBJECT; + // return () =>{ + // component().then(res => { + // LogUtils.log(res); + // result = res; + // }, reason => { + // LogUtils.log(reason); + // }); + // if(result === EMPTY_OBJECT){ + // throw component(); + // } + // return result; + // }; + // }); + + it('挂载lazy组件', async () => { + // 用同步的代码来实现异步操作 + class LazyComponent extends Horizon.Component { + render() { + return ; + } + } + + const Lazy = Horizon.lazy(() => mockImport(LazyComponent)); + + Horizon.render( + }> + + , + container + ); + + expect(LogUtils.getAndClear()).toEqual(['Loading...']); + expect(container.textContent).toBe('Loading...'); + + await Promise.resolve(); + Horizon.render( + }> + + , + container + ); + expect(LogUtils.getAndClear()).toEqual([5]); + expect(container.querySelector('p').innerHTML).toBe('5'); + }); + +}); diff --git a/scripts/__tests__/jest/testUtils.js b/scripts/__tests__/jest/testUtils.js old mode 100644 new mode 100755 index 9782ae05..6d4b7ac2 --- a/scripts/__tests__/jest/testUtils.js +++ b/scripts/__tests__/jest/testUtils.js @@ -7,26 +7,60 @@ export const stopBubbleOrCapture = (e, value) => { e.stopPropagation(); }; -export const getEventListeners = (dom) => { - let ret = true; - let keyArray = []; - for (let key in dom) { - keyArray.push(key); +function listAllEventListeners() { + const allElements = Array.prototype.slice.call(document.querySelectorAll('*')); + allElements.push(document); + allElements.push(window); + + const types = []; + + for (let ev in window) { + if (/^on/.test(ev)) types[types.length] = ev; } - console.log(keyArray); - console.log('---------------------------------'); - console.log(allDelegatedNativeEvents); - try { - allDelegatedNativeEvents.forEach(event => { - if (!keyArray.includes(event)) { - ret = false; - throw new Error('没有挂载全量事件'); + + let elements = []; + for (let i = 0; i < allElements.length; i++) { + const currentElement = allElements[i]; + for (let j = 0; j < types.length; j++) { + if (typeof currentElement[types[j]] === 'function') { + elements.push({ + 'node': currentElement, + 'type': types[j], + 'func': currentElement[types[j]].toString(), + }); } - }); - } catch (error) { - console.log(error); + } } - return ret; + + return elements.sort(function(a,b) { + return a.type.localeCompare(b.type); + }); +} + +export const getEventListeners = (dom) => { + console.table(listAllEventListeners()); + + + + // let ret = true; + // let keyArray = []; + // for (let key in dom) { + // if (/^on/.test(key)) keyArray.push(key); + // } + // console.log(getEventListeners); + // console.log('---------------------------------'); + // console.log(allDelegatedNativeEvents); + // try { + // allDelegatedNativeEvents.forEach(event => { + // if (!keyArray.includes(event)) { + // ret = false; + // throw new Error('没有挂载全量事件'); + // } + // }); + // } catch (error) { + // console.log(error); + // } + // return ret; }; export function triggerClickEvent(container, id) { @@ -58,6 +92,10 @@ class LogUtils { return values; }; + getNotClear = () => { + return this.dataArray === null ? [] : this.dataArray; + }; + clear = () => { this.dataArray = this.dataArray ? null : this.dataArray; };