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}
+