TestUtils.stopBubbleOrCapture(e, 'div capture')} onClick={() => LogUtils.log('div bubble')}>
+
TestUtils.stopBubbleOrCapture(e, 'div capture')}
+ onClick={() => LogUtils.log('div bubble')}
+ >
LogUtils.log('p capture')} onClick={() => LogUtils.log('p bubble')}>
@@ -99,7 +112,7 @@ describe('事件', () => {
expect(LogUtils.getAndClear()).toEqual([
// 阻止捕获,不再继续向下执行
- 'div capture'
+ 'div capture',
]);
});
@@ -114,19 +127,148 @@ describe('事件', () => {
);
};
Horizon.render(
, container);
- container.querySelector('div').addEventListener('click', () => {
- LogUtils.log('div bubble');
- }, false);
- container.querySelector('p').addEventListener('click', () => {
- LogUtils.log('p bubble');
- }, false);
- container.querySelector('button').addEventListener('click', (e) => {
- LogUtils.log('btn bubble');
- e.stopPropagation();
- }, false);
+ container.querySelector('div').addEventListener(
+ 'click',
+ () => {
+ LogUtils.log('div bubble');
+ },
+ false
+ );
+ container.querySelector('p').addEventListener(
+ 'click',
+ () => {
+ LogUtils.log('p bubble');
+ },
+ false
+ );
+ container.querySelector('button').addEventListener(
+ 'click',
+ e => {
+ LogUtils.log('btn bubble');
+ e.stopPropagation();
+ },
+ false
+ );
container.querySelector('button').click();
- expect(LogUtils.getAndClear()).toEqual([
- 'btn bubble'
- ]);
+ expect(LogUtils.getAndClear()).toEqual(['btn bubble']);
+ });
+
+ it('动态增加事件', () => {
+ let update;
+ let inputRef = Horizon.createRef();
+
+ function Test() {
+ const [inputProps, setProps] = Horizon.useState({});
+ update = setProps;
+ return
;
+ }
+
+ Horizon.render(
, container);
+ update({
+ onChange: () => {
+ LogUtils.log('change');
+ },
+ });
+ dispatchChangeEvent(inputRef.current);
+
+ expect(LogUtils.getAndClear()).toEqual(['change']);
+ });
+
+ it('Radio change事件', () => {
+ let radio1Called = 0;
+ let radio2Called = 0;
+
+ function onChange1() {
+ radio1Called++;
+ }
+
+ function onChange2() {
+ radio2Called++;
+ }
+
+ const radio1Ref = Horizon.createRef();
+ const radio2Ref = Horizon.createRef();
+
+ Horizon.render(
+ <>
+
+
+ >,
+ container
+ );
+
+ function clickRadioAndExpect(radio, [expect1, expect2]) {
+ radio.click();
+ expect(radio1Called).toBe(expect1);
+ expect(radio2Called).toBe(expect2);
+ }
+
+ // 先选择选项1
+ clickRadioAndExpect(radio1Ref.current, [1, 0]);
+
+ // 再选择选项1
+ clickRadioAndExpect(radio2Ref.current, [1, 1]);
+
+ // 先选择选项1,radio1应该重新触发onchange
+ clickRadioAndExpect(radio1Ref.current, [2, 1]);
+ });
+
+ it('多根节点下,事件挂载正确', () => {
+ const root1 = document.createElement('div');
+ const root2 = document.createElement('div');
+ root1.key = 'root1';
+ root2.key = 'root2';
+ let input1, input2, update1, update2;
+
+ function App1() {
+ const [props, setProps] = Horizon.useState({});
+ update1 = setProps;
+ return (
+
(input1 = n)}
+ onChange={() => {
+ LogUtils.log('input1 changed');
+ }}
+ />
+ );
+ }
+
+ function App2() {
+ const [props, setProps] = Horizon.useState({});
+ update2 = setProps;
+
+ return (
+
(input2 = n)}
+ onChange={() => {
+ LogUtils.log('input2 changed');
+ }}
+ />
+ );
+ }
+
+ // 多根mount阶段挂载onChange事件
+ Horizon.render(
, root1);
+ Horizon.render(
, root2);
+
+ dispatchChangeEvent(input1);
+ expect(LogUtils.getAndClear()).toEqual(['input1 changed']);
+ dispatchChangeEvent(input2);
+ expect(LogUtils.getAndClear()).toEqual(['input2 changed']);
+
+ // 多根update阶段挂载onClick事件
+ update1({
+ onClick: () => LogUtils.log('input1 clicked'),
+ });
+ update2({
+ onClick: () => LogUtils.log('input2 clicked'),
+ });
+
+ input1.click();
+ expect(LogUtils.getAndClear()).toEqual(['input1 clicked']);
+ input2.click();
+ expect(LogUtils.getAndClear()).toEqual(['input2 clicked']);
});
});
diff --git a/scripts/__tests__/HorizonXText/StateManager/StateArray.test.js b/scripts/__tests__/HorizonXText/StateManager/StateArray.test.js
new file mode 100644
index 00000000..826d6dda
--- /dev/null
+++ b/scripts/__tests__/HorizonXText/StateManager/StateArray.test.js
@@ -0,0 +1,201 @@
+import * as Horizon from '@cloudsop/horizon/index.ts';
+import { clearStore, createStore, useStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler';
+import { App, Text, triggerClickEvent } from '../../jest/commonComponents';
+
+describe('测试store中的Array', () => {
+ const { unmountComponentAtNode } = Horizon;
+ let container = null;
+ beforeEach(() => {
+ // 创建一个 DOM 元素作为渲染目标
+ container = document.createElement('div');
+ document.body.appendChild(container);
+
+ const persons = [
+ { name: 'p1', age: 1 },
+ { name: 'p2', age: 2 },
+ ];
+
+ createStore({
+ id: 'user',
+ state: {
+ type: 'bing dun dun',
+ persons: persons,
+ },
+ actions: {
+ addOnePerson: (state, person) => {
+ state.persons.push(person);
+ },
+ delOnePerson: state => {
+ state.persons.pop();
+ },
+ clearPersons: state => {
+ state.persons = null;
+ },
+ },
+ });
+ });
+
+ afterEach(() => {
+ // 退出时进行清理
+ unmountComponentAtNode(container);
+ container.remove();
+ container = null;
+
+ clearStore('user');
+ });
+
+ const newPerson = { name: 'p3', age: 3 };
+
+ function Parent(props) {
+ const userStore = useStore('user');
+ const addOnePerson = function() {
+ userStore.addOnePerson(newPerson);
+ };
+ const delOnePerson = function() {
+ userStore.delOnePerson();
+ };
+ return (
+
+
+
+
{props.children}
+
+ );
+ }
+
+ it('测试Array方法: push()、pop()', () => {
+ function Child(props) {
+ const userStore = useStore('user');
+
+ return (
+
+
+
+ );
+ }
+
+ Horizon.render(
, container);
+
+ expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: 2');
+ // 在Array中增加一个对象
+ Horizon.act(() => {
+ triggerClickEvent(container, 'addBtn');
+ });
+ expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: 3');
+
+ // 在Array中删除一个对象
+ Horizon.act(() => {
+ triggerClickEvent(container, 'delBtn');
+ });
+ expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: 2');
+ });
+
+ it('测试Array方法: entries()、push()、shift()、unshift、直接赋值', () => {
+ let globalStore = null;
+
+ function Child(props) {
+ const userStore = useStore('user');
+ globalStore = userStore;
+
+ const nameList = [];
+ const entries = userStore.$state.persons?.entries();
+ if (entries) {
+ for (const entry of entries) {
+ nameList.push(entry[1].name);
+ }
+ }
+
+ return (
+
+
+
+ );
+ }
+
+ Horizon.render(
, container);
+
+ expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2');
+ // push
+ globalStore.$state.persons.push(newPerson);
+ expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3');
+
+ // shift
+ globalStore.$state.persons.shift({ name: 'p0', age: 0 });
+ expect(container.querySelector('#nameList').innerHTML).toBe('name list: p2 p3');
+
+ // 赋值[2]
+ globalStore.$state.persons[2] = { name: 'p4', age: 4 };
+ expect(container.querySelector('#nameList').innerHTML).toBe('name list: p2 p3 p4');
+
+ // 重新赋值[2]
+ globalStore.$state.persons[2] = { name: 'p5', age: 5 };
+ expect(container.querySelector('#nameList').innerHTML).toBe('name list: p2 p3 p5');
+
+ // unshift
+ globalStore.$state.persons.unshift({ name: 'p1', age: 1 });
+ expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3 p5');
+
+ // 重新赋值 null
+ globalStore.$state.persons = null;
+ expect(container.querySelector('#nameList').innerHTML).toBe('name list: ');
+
+ // 重新赋值 [{ name: 'p1', age: 1 }]
+ globalStore.$state.persons = [{ name: 'p1', age: 1 }];
+ expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1');
+ });
+
+ it('测试Array方法: forEach()', () => {
+ let globalStore = null;
+
+ function Child(props) {
+ const userStore = useStore('user');
+ globalStore = userStore;
+
+ const nameList = [];
+ userStore.$state.persons?.forEach(per => {
+ nameList.push(per.name);
+ });
+
+ return (
+
+
+
+ );
+ }
+
+ Horizon.render(
, container);
+
+ expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2');
+ // push
+ globalStore.$state.persons.push(newPerson);
+ expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3');
+
+ // shift
+ globalStore.$state.persons.shift({ name: 'p0', age: 0 });
+ expect(container.querySelector('#nameList').innerHTML).toBe('name list: p2 p3');
+
+ // 赋值[2]
+ globalStore.$state.persons[2] = { name: 'p4', age: 4 };
+ expect(container.querySelector('#nameList').innerHTML).toBe('name list: p2 p3 p4');
+
+ // 重新赋值[2]
+ globalStore.$state.persons[2] = { name: 'p5', age: 5 };
+ expect(container.querySelector('#nameList').innerHTML).toBe('name list: p2 p3 p5');
+
+ // unshift
+ globalStore.$state.persons.unshift({ name: 'p1', age: 1 });
+ expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3 p5');
+
+ // 重新赋值 null
+ globalStore.$state.persons = null;
+ expect(container.querySelector('#nameList').innerHTML).toBe('name list: ');
+
+ // 重新赋值 [{ name: 'p1', age: 1 }]
+ globalStore.$state.persons = [{ name: 'p1', age: 1 }];
+ expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1');
+ });
+});
diff --git a/scripts/__tests__/HorizonXText/StateManager/StateMap.test.js b/scripts/__tests__/HorizonXText/StateManager/StateMap.test.js
new file mode 100644
index 00000000..f7eaed9f
--- /dev/null
+++ b/scripts/__tests__/HorizonXText/StateManager/StateMap.test.js
@@ -0,0 +1,323 @@
+import * as Horizon from '@cloudsop/horizon/index.ts';
+import { clearStore, createStore, useStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler';
+import { App, Text, triggerClickEvent } from '../../jest/commonComponents';
+
+describe('测试store中的Map', () => {
+ const { unmountComponentAtNode } = Horizon;
+ let container = null;
+ beforeEach(() => {
+ // 创建一个 DOM 元素作为渲染目标
+ container = document.createElement('div');
+ document.body.appendChild(container);
+
+ const persons = new Map([
+ ['p1', 1],
+ ['p2', 2],
+ ]);
+
+ createStore({
+ id: 'user',
+ state: {
+ type: 'bing dun dun',
+ persons: persons,
+ },
+ actions: {
+ addOnePerson: (state, person) => {
+ state.persons.set(person.name, person.age);
+ },
+ delOnePerson: (state, person) => {
+ state.persons.delete(person.name);
+ },
+ clearPersons: state => {
+ state.persons.clear();
+ },
+ },
+ });
+ });
+
+ afterEach(() => {
+ // 退出时进行清理
+ unmountComponentAtNode(container);
+ container.remove();
+ container = null;
+
+ clearStore('user');
+ });
+
+ const newPerson = { name: 'p3', age: 3 };
+
+ function Parent(props) {
+ const userStore = useStore('user');
+ const addOnePerson = function() {
+ userStore.addOnePerson(newPerson);
+ };
+ const delOnePerson = function() {
+ userStore.delOnePerson(newPerson);
+ };
+ const clearPersons = function() {
+ userStore.clearPersons();
+ };
+
+ return (
+
+
+
+
+
{props.children}
+
+ );
+ }
+
+ it('测试Map方法: set()、delete()、clear()', () => {
+ function Child(props) {
+ const userStore = useStore('user');
+
+ return (
+
+
+
+ );
+ }
+
+ Horizon.render(
, container);
+
+ expect(container.querySelector('#size').innerHTML).toBe('persons number: 2');
+ // 在Map中增加一个对象
+ Horizon.act(() => {
+ triggerClickEvent(container, 'addBtn');
+ });
+ expect(container.querySelector('#size').innerHTML).toBe('persons number: 3');
+
+ // 在Map中删除一个对象
+ Horizon.act(() => {
+ triggerClickEvent(container, 'delBtn');
+ });
+ expect(container.querySelector('#size').innerHTML).toBe('persons number: 2');
+
+ // clear Map
+ Horizon.act(() => {
+ triggerClickEvent(container, 'clearBtn');
+ });
+ expect(container.querySelector('#size').innerHTML).toBe('persons number: 0');
+ });
+
+ it('测试Map方法: keys()', () => {
+ function Child(props) {
+ const userStore = useStore('user');
+
+ const nameList = [];
+ const keys = userStore.$state.persons.keys();
+ for (const key of keys) {
+ nameList.push(key);
+ }
+
+ return (
+
+
+
+ );
+ }
+
+ Horizon.render(
, container);
+
+ expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2');
+ // 在Map中增加一个对象
+ Horizon.act(() => {
+ triggerClickEvent(container, 'addBtn');
+ });
+ expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3');
+
+ // 在Map中删除一个对象
+ Horizon.act(() => {
+ triggerClickEvent(container, 'delBtn');
+ });
+ expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2');
+
+ // clear Map
+ Horizon.act(() => {
+ triggerClickEvent(container, 'clearBtn');
+ });
+ expect(container.querySelector('#nameList').innerHTML).toBe('name list: ');
+ });
+
+ it('测试Map方法: values()', () => {
+ function Child(props) {
+ const userStore = useStore('user');
+
+ const ageList = [];
+ const values = userStore.$state.persons.values();
+ for (const val of values) {
+ ageList.push(val);
+ }
+
+ return (
+
+
+
+ );
+ }
+
+ Horizon.render(
, container);
+
+ expect(container.querySelector('#ageList').innerHTML).toBe('age list: 1 2');
+ // 在Map中增加一个对象
+ Horizon.act(() => {
+ triggerClickEvent(container, 'addBtn');
+ });
+ expect(container.querySelector('#ageList').innerHTML).toBe('age list: 1 2 3');
+
+ // 在Map中删除一个对象
+ Horizon.act(() => {
+ triggerClickEvent(container, 'delBtn');
+ });
+ expect(container.querySelector('#ageList').innerHTML).toBe('age list: 1 2');
+
+ // clear Map
+ Horizon.act(() => {
+ triggerClickEvent(container, 'clearBtn');
+ });
+ expect(container.querySelector('#ageList').innerHTML).toBe('age list: ');
+ });
+
+ it('测试Map方法: entries()', () => {
+ function Child(props) {
+ const userStore = useStore('user');
+
+ const nameList = [];
+ const entries = userStore.$state.persons.entries();
+ for (const entry of entries) {
+ nameList.push(entry[0]);
+ }
+
+ return (
+
+
+
+ );
+ }
+
+ Horizon.render(
, container);
+
+ expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2');
+ // 在Map中增加一个对象
+ Horizon.act(() => {
+ triggerClickEvent(container, 'addBtn');
+ });
+ expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3');
+
+ // 在Map中删除一个对象
+ Horizon.act(() => {
+ triggerClickEvent(container, 'delBtn');
+ });
+ expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2');
+
+ // clear Map
+ Horizon.act(() => {
+ triggerClickEvent(container, 'clearBtn');
+ });
+ expect(container.querySelector('#nameList').innerHTML).toBe('name list: ');
+ });
+
+ it('测试Map方法: forEach()', () => {
+ function Child(props) {
+ const userStore = useStore('user');
+
+ const nameList = [];
+ userStore.$state.persons.forEach((val, key) => {
+ nameList.push(key);
+ });
+
+ return (
+
+
+
+ );
+ }
+
+ Horizon.render(
, container);
+
+ expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2');
+ // 在Map中增加一个对象
+ Horizon.act(() => {
+ triggerClickEvent(container, 'addBtn');
+ });
+ expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3');
+
+ // 在Map中删除一个对象
+ Horizon.act(() => {
+ triggerClickEvent(container, 'delBtn');
+ });
+ expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2');
+
+ // clear Map
+ Horizon.act(() => {
+ triggerClickEvent(container, 'clearBtn');
+ });
+ expect(container.querySelector('#nameList').innerHTML).toBe('name list: ');
+ });
+
+ it('测试Map方法: has()', () => {
+ function Child(props) {
+ const userStore = useStore('user');
+
+ return (
+
+
+
+ );
+ }
+
+ Horizon.render(
, container);
+
+ expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: false');
+ // 在Map中增加一个对象
+ Horizon.act(() => {
+ triggerClickEvent(container, 'addBtn');
+ });
+ expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: true');
+ });
+
+ it('测试Map方法: for of()', () => {
+ function Child(props) {
+ const userStore = useStore('user');
+
+ const nameList = [];
+ for (const per of userStore.$state.persons) {
+ nameList.push(per[0]);
+ }
+
+ return (
+
+
+
+ );
+ }
+
+ Horizon.render(
, container);
+
+ expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2');
+ // 在Map中增加一个对象
+ Horizon.act(() => {
+ triggerClickEvent(container, 'addBtn');
+ });
+ expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3');
+
+ // 在Map中删除一个对象
+ Horizon.act(() => {
+ triggerClickEvent(container, 'delBtn');
+ });
+ expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2');
+
+ // clear Map
+ Horizon.act(() => {
+ triggerClickEvent(container, 'clearBtn');
+ });
+ expect(container.querySelector('#nameList').innerHTML).toBe('name list: ');
+ });
+});
diff --git a/scripts/__tests__/HorizonXText/StateManager/StateMixType.test.js b/scripts/__tests__/HorizonXText/StateManager/StateMixType.test.js
new file mode 100644
index 00000000..870d7d26
--- /dev/null
+++ b/scripts/__tests__/HorizonXText/StateManager/StateMixType.test.js
@@ -0,0 +1,164 @@
+import * as Horizon from '@cloudsop/horizon/index.ts';
+import { clearStore, createStore, useStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler';
+import { App, Text, triggerClickEvent } from '../../jest/commonComponents';
+
+describe('测试store中的混合类型变化', () => {
+ const { unmountComponentAtNode } = Horizon;
+ let container = null;
+ beforeEach(() => {
+ // 创建一个 DOM 元素作为渲染目标
+ container = document.createElement('div');
+ document.body.appendChild(container);
+
+ const persons = new Set([{ name: 'p1', age: 1, love: new Map() }]);
+ persons.add({
+ name: 'p2',
+ age: 2,
+ love: new Map(),
+ });
+ persons
+ .values()
+ .next()
+ .value.love.set('lanqiu', { moneny: 100, days: [1, 3, 5] });
+
+ createStore({
+ id: 'user',
+ state: {
+ type: 'bing dun dun',
+ persons: persons,
+ },
+ actions: {
+ addDay: (state, day) => {
+ state.persons
+ .values()
+ .next()
+ .value.love.get('lanqiu')
+ .days.push(day);
+ },
+ },
+ });
+ });
+
+ afterEach(() => {
+ // 退出时进行清理
+ unmountComponentAtNode(container);
+ container.remove();
+ container = null;
+
+ clearStore('user');
+ });
+
+ function Parent(props) {
+ const userStore = useStore('user');
+ const addDay = function() {
+ userStore.addDay(7);
+ };
+
+ return (
+
+
+
{props.children}
+
+ );
+ }
+
+ it('测试state -> set -> map -> array的数据变化', () => {
+ function Child(props) {
+ const userStore = useStore('user');
+
+ const days = userStore.$state.persons
+ .values()
+ .next()
+ .value.love.get('lanqiu').days;
+
+ return (
+
+
+
+ );
+ }
+
+ Horizon.render(
, container);
+
+ expect(container.querySelector('#dayList').innerHTML).toBe('love: 1 3 5');
+ Horizon.act(() => {
+ triggerClickEvent(container, 'addBtn');
+ });
+ expect(container.querySelector('#dayList').innerHTML).toBe('love: 1 3 5 7');
+ });
+
+ it('属性是个class实例', () => {
+ class Person {
+ name;
+ age;
+ loves = new Set();
+
+ constructor(name, age) {
+ this.name = name;
+ this.age = age;
+ }
+
+ setName(name) {
+ this.name = name;
+ }
+
+ getName() {
+ return this.name;
+ }
+
+ setAge(age) {
+ this.age = age;
+ }
+
+ getAge() {
+ return this.age;
+ }
+
+ addLove(lv) {
+ this.loves.add(lv);
+ }
+
+ getLoves() {
+ return this.loves;
+ }
+ }
+
+ let globalPerson;
+ let globalStore;
+
+ function Child(props) {
+ const userStore = useStore('user');
+ globalStore = userStore;
+
+ const nameList = [];
+ const valIterator = userStore.$state.persons.values();
+ let per = valIterator.next();
+ while (!per.done) {
+ nameList.push(per.value.name ?? per.value.getName());
+ globalPerson = per.value;
+ per = valIterator.next();
+ }
+
+ return (
+
+
+
+ );
+ }
+
+ Horizon.render(
, container);
+
+ expect(container.querySelector('#nameList').innerHTML).toBe('p1 p2');
+
+ // 动态增加一个Person实例
+ globalStore.$state.persons.add(new Person('ClassPerson', 5));
+
+ expect(container.querySelector('#nameList').innerHTML).toBe('p1 p2 ClassPerson');
+
+ globalPerson.setName('ClassPerson1');
+
+ expect(container.querySelector('#nameList').innerHTML).toBe('p1 p2 ClassPerson1');
+ });
+});
diff --git a/scripts/__tests__/HorizonXText/StateManager/StateSet.test.js b/scripts/__tests__/HorizonXText/StateManager/StateSet.test.js
new file mode 100644
index 00000000..4b6bdc7b
--- /dev/null
+++ b/scripts/__tests__/HorizonXText/StateManager/StateSet.test.js
@@ -0,0 +1,294 @@
+import * as Horizon from '@cloudsop/horizon/index.ts';
+import { clearStore, createStore, useStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler';
+import { App, Text, triggerClickEvent } from '../../jest/commonComponents';
+
+describe('测试store中的Set', () => {
+ const { unmountComponentAtNode } = Horizon;
+ let container = null;
+ beforeEach(() => {
+ // 创建一个 DOM 元素作为渲染目标
+ container = document.createElement('div');
+ document.body.appendChild(container);
+
+ const persons = new Set([
+ { name: 'p1', age: 1 },
+ { name: 'p2', age: 2 },
+ ]);
+
+ createStore({
+ id: 'user',
+ state: {
+ type: 'bing dun dun',
+ persons: persons,
+ },
+ actions: {
+ addOnePerson: (state, person) => {
+ state.persons.add(person);
+ },
+ delOnePerson: (state, person) => {
+ state.persons.delete(person);
+ },
+ clearPersons: state => {
+ state.persons.clear();
+ },
+ },
+ });
+ });
+
+ afterEach(() => {
+ // 退出时进行清理
+ unmountComponentAtNode(container);
+ container.remove();
+ container = null;
+
+ clearStore('user');
+ });
+
+ const newPerson = { name: 'p3', age: 3 };
+
+ function Parent(props) {
+ const userStore = useStore('user');
+ const addOnePerson = function() {
+ userStore.addOnePerson(newPerson);
+ };
+ const delOnePerson = function() {
+ userStore.delOnePerson(newPerson);
+ };
+ const clearPersons = function() {
+ userStore.clearPersons();
+ };
+
+ return (
+
+
+
+
+
{props.children}
+
+ );
+ }
+
+ it('测试Set方法: add()、delete()、clear()', () => {
+ function Child(props) {
+ const userStore = useStore('user');
+ const personArr = Array.from(userStore.$state.persons);
+ const nameList = [];
+ const keys = userStore.$state.persons.keys();
+ for (const key of keys) {
+ nameList.push(key.name);
+ }
+
+ return (
+
+
+
+
+ );
+ }
+
+ Horizon.render(
, container);
+
+ expect(container.querySelector('#size').innerHTML).toBe('persons number: 2');
+ expect(container.querySelector('#lastAge').innerHTML).toBe('last person age: 2');
+ // 在set中增加一个对象
+ Horizon.act(() => {
+ triggerClickEvent(container, 'addBtn');
+ });
+ expect(container.querySelector('#size').innerHTML).toBe('persons number: 3');
+
+ // 在set中删除一个对象
+ Horizon.act(() => {
+ triggerClickEvent(container, 'delBtn');
+ });
+ expect(container.querySelector('#size').innerHTML).toBe('persons number: 2');
+
+ // clear set
+ Horizon.act(() => {
+ triggerClickEvent(container, 'clearBtn');
+ });
+ expect(container.querySelector('#size').innerHTML).toBe('persons number: 0');
+ expect(container.querySelector('#lastAge').innerHTML).toBe('last person age: 0');
+ });
+
+ it('测试Set方法: keys()、values()', () => {
+ function Child(props) {
+ const userStore = useStore('user');
+
+ const nameList = [];
+ const keys = userStore.$state.persons.keys();
+ // const keys = userStore.$state.persons.values();
+ for (const key of keys) {
+ nameList.push(key.name);
+ }
+
+ return (
+
+
+
+ );
+ }
+
+ Horizon.render(
, container);
+
+ expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2');
+ // 在set中增加一个对象
+ Horizon.act(() => {
+ triggerClickEvent(container, 'addBtn');
+ });
+ expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3');
+
+ // 在set中删除一个对象
+ Horizon.act(() => {
+ triggerClickEvent(container, 'delBtn');
+ });
+ expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2');
+
+ // clear set
+ Horizon.act(() => {
+ triggerClickEvent(container, 'clearBtn');
+ });
+ expect(container.querySelector('#nameList').innerHTML).toBe('name list: ');
+ });
+
+ it('测试Set方法: entries()', () => {
+ function Child(props) {
+ const userStore = useStore('user');
+
+ const nameList = [];
+ const entries = userStore.$state.persons.entries();
+ for (const entry of entries) {
+ nameList.push(entry[0].name);
+ }
+
+ return (
+
+
+
+ );
+ }
+
+ Horizon.render(
, container);
+
+ expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2');
+ // 在set中增加一个对象
+ Horizon.act(() => {
+ triggerClickEvent(container, 'addBtn');
+ });
+ expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3');
+
+ // 在set中删除一个对象
+ Horizon.act(() => {
+ triggerClickEvent(container, 'delBtn');
+ });
+ expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2');
+
+ // clear set
+ Horizon.act(() => {
+ triggerClickEvent(container, 'clearBtn');
+ });
+ expect(container.querySelector('#nameList').innerHTML).toBe('name list: ');
+ });
+
+ it('测试Set方法: forEach()', () => {
+ function Child(props) {
+ const userStore = useStore('user');
+
+ const nameList = [];
+ userStore.$state.persons.forEach(per => {
+ nameList.push(per.name);
+ });
+
+ return (
+
+
+
+ );
+ }
+
+ Horizon.render(
, container);
+
+ expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2');
+ // 在set中增加一个对象
+ Horizon.act(() => {
+ triggerClickEvent(container, 'addBtn');
+ });
+ expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3');
+
+ // 在set中删除一个对象
+ Horizon.act(() => {
+ triggerClickEvent(container, 'delBtn');
+ });
+ expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2');
+
+ // clear set
+ Horizon.act(() => {
+ triggerClickEvent(container, 'clearBtn');
+ });
+ expect(container.querySelector('#nameList').innerHTML).toBe('name list: ');
+ });
+
+ it('测试Set方法: has()', () => {
+ function Child(props) {
+ const userStore = useStore('user');
+
+ return (
+
+
+
+ );
+ }
+
+ Horizon.render(
, container);
+
+ expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: false');
+ // 在set中增加一个对象
+ Horizon.act(() => {
+ triggerClickEvent(container, 'addBtn');
+ });
+ expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: true');
+ });
+
+ it('测试Set方法: for of()', () => {
+ function Child(props) {
+ const userStore = useStore('user');
+
+ const nameList = [];
+ for (const per of userStore.$state.persons) {
+ nameList.push(per.name);
+ }
+
+ return (
+
+
+
+ );
+ }
+
+ Horizon.render(
, container);
+
+ expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2');
+ // 在set中增加一个对象
+ Horizon.act(() => {
+ triggerClickEvent(container, 'addBtn');
+ });
+ expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3');
+
+ // 在set中删除一个对象
+ Horizon.act(() => {
+ triggerClickEvent(container, 'delBtn');
+ });
+ expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2');
+
+ // clear set
+ Horizon.act(() => {
+ triggerClickEvent(container, 'clearBtn');
+ });
+ expect(container.querySelector('#nameList').innerHTML).toBe('name list: ');
+ });
+});
diff --git a/scripts/__tests__/HorizonXText/StateManager/StateWeakMap.test.js b/scripts/__tests__/HorizonXText/StateManager/StateWeakMap.test.js
new file mode 100644
index 00000000..28d4ff98
--- /dev/null
+++ b/scripts/__tests__/HorizonXText/StateManager/StateWeakMap.test.js
@@ -0,0 +1,124 @@
+import * as Horizon from '@cloudsop/horizon/index.ts';
+import { clearStore, createStore, useStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler';
+import { App, Text, triggerClickEvent } from '../../jest/commonComponents';
+
+describe('测试store中的WeakMap', () => {
+ const { unmountComponentAtNode } = Horizon;
+ let container = null;
+ beforeEach(() => {
+ // 创建一个 DOM 元素作为渲染目标
+ container = document.createElement('div');
+ document.body.appendChild(container);
+
+ const persons = new WeakMap([
+ [{ name: 'p1' }, 1],
+ [{ name: 'p2' }, 2],
+ ]);
+
+ createStore({
+ id: 'user',
+ state: {
+ type: 'bing dun dun',
+ persons: persons,
+ },
+ actions: {
+ addOnePerson: (state, person) => {
+ state.persons.set(person, 3);
+ },
+ delOnePerson: (state, person) => {
+ state.persons.delete(person);
+ },
+ clearPersons: state => {
+ state.persons.clear();
+ },
+ },
+ });
+ });
+
+ afterEach(() => {
+ // 退出时进行清理
+ unmountComponentAtNode(container);
+ container.remove();
+ container = null;
+
+ clearStore('user');
+ });
+
+ const newPerson = { name: 'p3' };
+
+ function Parent(props) {
+ const userStore = useStore('user');
+ const addOnePerson = function() {
+ userStore.addOnePerson(newPerson);
+ };
+ const delOnePerson = function() {
+ userStore.delOnePerson(newPerson);
+ };
+ const clearPersons = function() {
+ userStore.clearPersons();
+ };
+
+ return (
+
+
+
+
+
{props.children}
+
+ );
+ }
+
+ it('测试WeakMap方法: set()、delete()、has()', () => {
+ function Child(props) {
+ const userStore = useStore('user');
+
+ return (
+
+
+
+ );
+ }
+
+ Horizon.render(
, container);
+
+ expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: false');
+ // 在WeakMap中增加一个对象
+ Horizon.act(() => {
+ triggerClickEvent(container, 'addBtn');
+ });
+ expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: true');
+
+ // 在WeakMap中删除一个对象
+ Horizon.act(() => {
+ triggerClickEvent(container, 'delBtn');
+ });
+ expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: false');
+ });
+
+ it('测试WeakMap方法: get()', () => {
+ function Child(props) {
+ const userStore = useStore('user');
+
+ return (
+
+
+
+ );
+ }
+
+ Horizon.render(
, container);
+
+ expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: undefined');
+ // 在WeakMap中增加一个对象
+ Horizon.act(() => {
+ triggerClickEvent(container, 'addBtn');
+ });
+ expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: 3');
+ });
+});
diff --git a/scripts/__tests__/HorizonXText/StateManager/StateWeakSet.test.js b/scripts/__tests__/HorizonXText/StateManager/StateWeakSet.test.js
new file mode 100644
index 00000000..4c6a8fca
--- /dev/null
+++ b/scripts/__tests__/HorizonXText/StateManager/StateWeakSet.test.js
@@ -0,0 +1,96 @@
+import * as Horizon from '@cloudsop/horizon/index.ts';
+import { clearStore, createStore, useStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler';
+import { App, Text, triggerClickEvent } from '../../jest/commonComponents';
+
+describe('测试store中的WeakSet', () => {
+ const { unmountComponentAtNode } = Horizon;
+ let container = null;
+ beforeEach(() => {
+ // 创建一个 DOM 元素作为渲染目标
+ container = document.createElement('div');
+ document.body.appendChild(container);
+
+ const persons = new WeakSet([
+ { name: 'p1', age: 1 },
+ { name: 'p2', age: 2 },
+ ]);
+
+ createStore({
+ id: 'user',
+ state: {
+ type: 'bing dun dun',
+ persons: persons,
+ },
+ actions: {
+ addOnePerson: (state, person) => {
+ state.persons.add(person);
+ },
+ delOnePerson: (state, person) => {
+ state.persons.delete(person);
+ },
+ clearPersons: state => {
+ state.persons.clear();
+ },
+ },
+ });
+ });
+
+ afterEach(() => {
+ // 退出时进行清理
+ unmountComponentAtNode(container);
+ container.remove();
+ container = null;
+
+ clearStore('user');
+ });
+
+ const newPerson = { name: 'p3', age: 3 };
+
+ function Parent(props) {
+ const userStore = useStore('user');
+ const addOnePerson = function() {
+ userStore.addOnePerson(newPerson);
+ };
+ const delOnePerson = function() {
+ userStore.delOnePerson(newPerson);
+ };
+ return (
+
+
+
+
{props.children}
+
+ );
+ }
+
+ it('测试WeakSet方法: add()、delete()、has()', () => {
+ function Child(props) {
+ const userStore = useStore('user');
+
+ return (
+
+
+
+ );
+ }
+
+ Horizon.render(
, container);
+
+ expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: false');
+ // 在WeakSet中增加一个对象
+ Horizon.act(() => {
+ triggerClickEvent(container, 'addBtn');
+ });
+ expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: true');
+
+ // 在WeakSet中删除一个对象
+ Horizon.act(() => {
+ triggerClickEvent(container, 'delBtn');
+ });
+ expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: false');
+ });
+});
diff --git a/scripts/__tests__/HorizonXText/StoreFunctionality/async.test.js b/scripts/__tests__/HorizonXText/StoreFunctionality/async.test.js
new file mode 100644
index 00000000..f45599d2
--- /dev/null
+++ b/scripts/__tests__/HorizonXText/StoreFunctionality/async.test.js
@@ -0,0 +1,161 @@
+import * as Horizon from '@cloudsop/horizon/index.ts';
+import { createStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler';
+import { triggerClickEvent } from '../../jest/commonComponents';
+
+const { unmountComponentAtNode } = Horizon;
+
+function postpone(timer, func) {
+ return new Promise(resolve => {
+ setTimeout(function() {
+ resolve(func());
+ }, timer);
+ });
+}
+
+describe('Asynchronous functions', () => {
+ let container = null;
+
+ const COUNTER_ID = 'counter';
+ const TOGGLE_ID = 'toggle';
+ const TOGGLE_FAST_ID = 'toggleFast';
+ const RESULT_ID = 'result';
+
+ let useAsyncCounter;
+
+ beforeEach(() => {
+ useAsyncCounter = createStore({
+ state: {
+ counter: 0,
+ check: false,
+ },
+ actions: {
+ increment: function(state) {
+ return new Promise(resolve => {
+ setTimeout(() => {
+ state.counter++;
+ resolve();
+ }, 100);
+ });
+ },
+ toggle: function(state) {
+ state.check = !state.check;
+ },
+ },
+ computed: {
+ value: state => {
+ return (state.check ? 'true' : 'false') + state.counter;
+ },
+ },
+ });
+ container = document.createElement('div');
+ document.body.appendChild(container);
+ });
+
+ afterEach(() => {
+ unmountComponentAtNode(container);
+ container.remove();
+ container = null;
+ });
+
+ it('Should wait for async actions', async () => {
+ jest.useRealTimers();
+ let globalStore;
+
+ function App() {
+ const store = useAsyncCounter();
+ globalStore = store;
+
+ return (
+
+
{store.value}
+
+
+
+
+ );
+ }
+
+ Horizon.render(
, container);
+
+ // initial state
+ expect(document.getElementById(RESULT_ID).innerHTML).toBe('false0');
+
+ // slow toggle has nothing to wait for, it is resolved immediately
+ Horizon.act(() => {
+ triggerClickEvent(container, TOGGLE_ID);
+ });
+
+ expect(document.getElementById(RESULT_ID).innerHTML).toBe('true0');
+
+ // counter increment is slow. slow toggle waits for result
+ Horizon.act(() => {
+ triggerClickEvent(container, COUNTER_ID);
+ });
+ Horizon.act(() => {
+ triggerClickEvent(container, TOGGLE_ID);
+ });
+
+ expect(document.getElementById(RESULT_ID).innerHTML).toBe('true0');
+
+ // fast toggle does not wait for counter and it is resolved immediately
+ Horizon.act(() => {
+ triggerClickEvent(container, TOGGLE_FAST_ID);
+ });
+
+ expect(document.getElementById(RESULT_ID).innerHTML).toBe('false0');
+
+ // at 150ms counter increment will be resolved and slow toggle immediately after
+ const t150 = postpone(150, () => {
+ expect(document.getElementById(RESULT_ID).innerHTML).toBe('true1');
+ });
+
+ // before that, two more actions are added to queue - another counter and slow toggle
+ Horizon.act(() => {
+ triggerClickEvent(container, COUNTER_ID);
+ });
+ Horizon.act(() => {
+ triggerClickEvent(container, TOGGLE_ID);
+ });
+
+ // at 250ms they should be already resolved
+ const t250 = postpone(250, () => {
+ expect(document.getElementById(RESULT_ID).innerHTML).toBe('false2');
+ });
+
+ await Promise.all([t150, t250]);
+ });
+
+ it('call async action by then', async () => {
+ jest.useFakeTimers();
+ let globalStore;
+
+ function App() {
+ const store = useAsyncCounter();
+ globalStore = store;
+
+ return (
+
+ );
+ }
+
+ Horizon.render(
, container);
+
+ // call async action by then
+ globalStore.$queue.increment().then(() => {
+ expect(document.getElementById(RESULT_ID).innerHTML).toBe('false1');
+ });
+
+ expect(document.getElementById(RESULT_ID).innerHTML).toBe('false0');
+
+ // past 150 ms
+ jest.advanceTimersByTime(150);
+ });
+});
diff --git a/scripts/__tests__/HorizonXText/StoreFunctionality/basicAccess.test.js b/scripts/__tests__/HorizonXText/StoreFunctionality/basicAccess.test.js
new file mode 100644
index 00000000..e42feeda
--- /dev/null
+++ b/scripts/__tests__/HorizonXText/StoreFunctionality/basicAccess.test.js
@@ -0,0 +1,63 @@
+import * as Horizon from '@cloudsop/horizon/index.ts';
+import { triggerClickEvent } from '../../jest/commonComponents';
+import { useLogStore } from './store';
+
+const { unmountComponentAtNode } = Horizon;
+
+describe('Basic store manipulation', () => {
+ let container = null;
+
+ const BUTTON_ID = 'btn';
+ const RESULT_ID = 'result';
+
+ beforeEach(() => {
+ container = document.createElement('div');
+ document.body.appendChild(container);
+ });
+
+ afterEach(() => {
+ unmountComponentAtNode(container);
+ container.remove();
+ container = null;
+ });
+
+ it('Should use getters', () => {
+ function App() {
+ const logStore = useLogStore();
+
+ return
{logStore.length}
;
+ }
+
+ Horizon.render(
, container);
+
+ expect(document.getElementById(RESULT_ID).innerHTML).toBe('1');
+ });
+
+ it('Should use actions and update components', () => {
+ function App() {
+ const logStore = useLogStore();
+
+ return (
+
+
+
{logStore.length}
+
+ );
+ }
+
+ Horizon.render(
, container);
+
+ Horizon.act(() => {
+ triggerClickEvent(container, BUTTON_ID);
+ });
+
+ expect(document.getElementById(RESULT_ID).innerHTML).toBe('2');
+ });
+});
diff --git a/scripts/__tests__/HorizonXText/StoreFunctionality/dollarAccess.test.js b/scripts/__tests__/HorizonXText/StoreFunctionality/dollarAccess.test.js
new file mode 100644
index 00000000..032234d0
--- /dev/null
+++ b/scripts/__tests__/HorizonXText/StoreFunctionality/dollarAccess.test.js
@@ -0,0 +1,63 @@
+import * as Horizon from '@cloudsop/horizon/index.ts';
+import { triggerClickEvent } from '../../jest/commonComponents';
+import { useLogStore } from './store';
+
+const { unmountComponentAtNode } = Horizon;
+
+describe('Dollar store access', () => {
+ let container = null;
+
+ const BUTTON_ID = 'btn';
+ const RESULT_ID = 'result';
+
+ beforeEach(() => {
+ container = document.createElement('div');
+ document.body.appendChild(container);
+ });
+
+ afterEach(() => {
+ unmountComponentAtNode(container);
+ container.remove();
+ container = null;
+ });
+
+ it('Should use $state and $computed', () => {
+ function App() {
+ const logStore = useLogStore();
+
+ return
{logStore.$computed.length()}
;
+ }
+
+ Horizon.render(
, container);
+
+ expect(document.getElementById(RESULT_ID).innerHTML).toBe('1');
+ });
+
+ it('Should use $actions and update components', () => {
+ function App() {
+ const logStore = useLogStore();
+
+ return (
+
+
+
{logStore.$computed.length()}
+
+ );
+ }
+
+ Horizon.render(
, container);
+
+ Horizon.act(() => {
+ triggerClickEvent(container, BUTTON_ID);
+ });
+
+ expect(document.getElementById(RESULT_ID).innerHTML).toBe('2');
+ });
+});
diff --git a/scripts/__tests__/HorizonXText/StoreFunctionality/otherCases.test.js b/scripts/__tests__/HorizonXText/StoreFunctionality/otherCases.test.js
new file mode 100644
index 00000000..115164c0
--- /dev/null
+++ b/scripts/__tests__/HorizonXText/StoreFunctionality/otherCases.test.js
@@ -0,0 +1,148 @@
+import * as Horizon from '@cloudsop/horizon/index.ts';
+import { createStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler';
+import { triggerClickEvent } from '../../jest/commonComponents';
+
+const { unmountComponentAtNode } = Horizon;
+
+describe('Self referencing', () => {
+ let container = null;
+
+ const BUTTON_ID = 'btn';
+ const RESULT_ID = 'result';
+
+ const useSelfRefStore = createStore({
+ state: {
+ val: 2,
+ },
+ actions: {
+ magic: function(state) {
+ state.val = state.val * 2 - 1;
+ },
+ },
+ computed: {
+ value: state => state.val,
+ double: function() {
+ return this.value * 2;
+ },
+ },
+ });
+
+ beforeEach(() => {
+ container = document.createElement('div');
+ document.body.appendChild(container);
+ });
+
+ afterEach(() => {
+ unmountComponentAtNode(container);
+ container.remove();
+ container = null;
+ });
+
+ it('Should use own getters', () => {
+ function App() {
+ const store = useSelfRefStore();
+
+ return (
+
+
{store.double}
+
+
+ );
+ }
+
+ Horizon.render(
, container);
+
+ expect(document.getElementById(RESULT_ID).innerHTML).toBe('4');
+
+ Horizon.act(() => {
+ triggerClickEvent(container, BUTTON_ID);
+ });
+
+ expect(document.getElementById(RESULT_ID).innerHTML).toBe('6');
+
+ Horizon.act(() => {
+ triggerClickEvent(container, BUTTON_ID);
+ });
+
+ expect(document.getElementById(RESULT_ID).innerHTML).toBe('10');
+ });
+
+ it('should access other stores', () => {
+ const useOtherStore = createStore({
+ state: {},
+ actions: {
+ doMagic: () => useSelfRefStore().magic(),
+ },
+ computed: {
+ magicConstant: () => useSelfRefStore().value,
+ },
+ });
+
+ function App() {
+ const store = useOtherStore();
+
+ return (
+
+
{store.magicConstant}
+
+
+ );
+ }
+
+ Horizon.render(
, container);
+
+ expect(document.getElementById(RESULT_ID).innerHTML).toBe('5');
+
+ Horizon.act(() => {
+ triggerClickEvent(container, BUTTON_ID);
+ });
+
+ expect(document.getElementById(RESULT_ID).innerHTML).toBe('9');
+ });
+
+ it('should use parametric getters', () => {
+ const useArrayStore = createStore({
+ state: {
+ items: ['a', 'b', 'c'],
+ },
+ actions: {
+ setItem: (state, index, value) => (state.items[index] = value),
+ },
+ computed: {
+ getItem: state => index => state.items[index],
+ },
+ });
+
+ function App() {
+ const store = useArrayStore();
+
+ return (
+
+
{store.getItem(0) + store.getItem(1) + store.getItem(2)}
+
+
+ );
+ }
+
+ Horizon.render(
, container);
+ expect(document.getElementById(RESULT_ID).innerHTML).toBe('abc');
+
+ Horizon.act(() => {
+ triggerClickEvent(container, BUTTON_ID);
+ });
+ expect(document.getElementById(RESULT_ID).innerHTML).toBe('def');
+ });
+});
diff --git a/scripts/__tests__/HorizonXText/StoreFunctionality/reset.test.js b/scripts/__tests__/HorizonXText/StoreFunctionality/reset.test.js
new file mode 100644
index 00000000..f0b349da
--- /dev/null
+++ b/scripts/__tests__/HorizonXText/StoreFunctionality/reset.test.js
@@ -0,0 +1,89 @@
+import * as Horizon from '@cloudsop/horizon/index.ts';
+import { createStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler';
+import { triggerClickEvent } from '../../jest/commonComponents';
+
+const { unmountComponentAtNode } = Horizon;
+
+describe('Reset', () => {
+ it('RESET NOT IMPLEMENTED', async () => {
+ // console.log('reset functionality is not yet implemented')
+ expect(true).toBe(true);
+ });
+ return;
+
+ let container = null;
+
+ const BUTTON_ID = 'btn';
+ const RESET_ID = 'reset';
+ const RESULT_ID = 'result';
+
+ const useCounter = createStore({
+ state: {
+ counter: 0,
+ },
+ actions: {
+ increment: function(state) {
+ state.counter++;
+ },
+ },
+ computed: {},
+ });
+
+ beforeEach(() => {
+ container = document.createElement('div');
+ document.body.appendChild(container);
+ });
+
+ afterEach(() => {
+ unmountComponentAtNode(container);
+ container.remove();
+ container = null;
+ });
+
+ it('Should reset to default state', async () => {
+ function App() {
+ const store = useCounter();
+
+ return (
+
+
{store.$state.counter}
+
+
+
+ );
+ }
+
+ Horizon.render(
, container);
+
+ Horizon.act(() => {
+ triggerClickEvent(container, BUTTON_ID);
+ });
+
+ Horizon.act(() => {
+ triggerClickEvent(container, BUTTON_ID);
+ });
+
+ expect(document.getElementById(RESULT_ID).innerHTML).toBe('2');
+
+ Horizon.act(() => {
+ triggerClickEvent(container, RESET_ID);
+ });
+
+ expect(document.getElementById(RESULT_ID).innerHTML).toBe('0');
+
+ Horizon.act(() => {
+ triggerClickEvent(container, BUTTON_ID);
+ });
+
+ expect(document.getElementById(RESULT_ID).innerHTML).toBe('1');
+ });
+});
diff --git a/scripts/__tests__/HorizonXText/StoreFunctionality/store.js b/scripts/__tests__/HorizonXText/StoreFunctionality/store.js
new file mode 100644
index 00000000..ccdbc6a6
--- /dev/null
+++ b/scripts/__tests__/HorizonXText/StoreFunctionality/store.js
@@ -0,0 +1,25 @@
+import { createStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler';
+
+export const useLogStore = createStore({
+ id: 'logStore', // you do not need to specify ID for local store
+ state: {
+ logs: ['log'],
+ },
+ actions: {
+ addLog: (state, data) => {
+ state.logs.push(data);
+ },
+ removeLog: (state, index) => {
+ state.logs.splice(index, 1);
+ },
+ cleanLog: state => {
+ state.logs.length = 0;
+ },
+ },
+ computed: {
+ length: state => {
+ return state.logs.length;
+ },
+ log: state => index => state.logs[index],
+ },
+});
diff --git a/scripts/__tests__/HorizonXText/adapters/ReduxAdapter.test.js b/scripts/__tests__/HorizonXText/adapters/ReduxAdapter.test.js
new file mode 100644
index 00000000..2fa93598
--- /dev/null
+++ b/scripts/__tests__/HorizonXText/adapters/ReduxAdapter.test.js
@@ -0,0 +1,208 @@
+import {
+ createStore,
+ applyMiddleware,
+ combineReducers,
+ bindActionCreators
+} from '../../../../libs/horizon/src/horizonx/adapters/redux';
+
+describe('Redux adapter', () => {
+ it('should use getState()', async () => {
+ const reduxStore = createStore((state, action) => {
+ return state;
+ }, 0);
+
+ expect(reduxStore.getState()).toBe(0);
+ })
+
+ it('Should use default state, dispatch action and update state', async () => {
+ const reduxStore = createStore((state, action) => {
+ switch (action.type) {
+ case('ADD'):
+ return {counter: state.counter + 1}
+ default:
+ return {counter: 0};
+ }
+ });
+
+ expect(reduxStore.getState().counter).toBe(0);
+
+ reduxStore.dispatch({type: 'ADD'});
+
+ expect(reduxStore.getState().counter).toBe(1);
+ });
+
+ it('Should attach and detach listeners', async () => {
+ let counter = 0;
+ const reduxStore = createStore((state = 0, action) => {
+ switch (action.type) {
+ case('ADD'):
+ return state + 1
+ default:
+ return state;
+ }
+ });
+
+ reduxStore.dispatch({type: 'ADD'});
+ expect(counter).toBe(0);
+ expect(reduxStore.getState()).toBe(1);
+ const unsubscribe = reduxStore.subscribe(() => {
+ counter++;
+ });
+ reduxStore.dispatch({type: 'ADD'});
+ reduxStore.dispatch({type: 'ADD'});
+ expect(counter).toBe(2);
+ expect(reduxStore.getState()).toBe(3);
+ unsubscribe();
+ reduxStore.dispatch({type: 'ADD'});
+ reduxStore.dispatch({type: 'ADD'});
+ expect(counter).toBe(2);
+ expect(reduxStore.getState()).toBe(5);
+ });
+
+ it('Should bind action creators', async () => {
+ const addTodo = (text) => {
+ return {
+ type: 'ADD_TODO',
+ text
+ }
+ }
+
+ const reduxStore = createStore((state = [], action) => {
+ if (action.type === 'ADD_TODO') {
+ return [...state, action.text];
+ }
+ return state;
+ });
+
+ const actions = bindActionCreators({addTodo}, reduxStore.dispatch);
+
+ actions.addTodo('todo');
+
+ expect(reduxStore.getState()[0]).toBe('todo');
+ });
+
+ it('Should replace reducer', async () => {
+ const reduxStore = createStore((state, action) => {
+ switch (action.type) {
+ case('ADD'):
+ return {counter: state.counter + 1}
+ default:
+ return {counter: 0};
+ }
+ });
+
+ reduxStore.dispatch({type: 'ADD'});
+
+ expect(reduxStore.getState().counter).toBe(1);
+
+ reduxStore.replaceReducer((state, action) => {
+ switch (action.type) {
+ case('SUB'):
+ return {counter: state.counter - 1}
+ default:
+ return {counter: 0};
+ }
+ });
+
+ reduxStore.dispatch({type: 'SUB'});
+
+ expect(reduxStore.getState().counter).toBe(0);
+ })
+
+ it('Should combine reducers', async () => {
+ const booleanReducer = (state = false, action) => {
+ switch (action.type) {
+ case('TOGGLE'):
+ return !state
+ default:
+ return state;
+ }
+ }
+
+ const addReducer = (state = 0, action) => {
+ switch (action.type) {
+ case('ADD'):
+ return state + 1
+ default:
+ return state;
+ }
+ };
+
+ const reduxStore = createStore(combineReducers({check: booleanReducer, counter: addReducer}));
+
+ expect(reduxStore.getState().counter).toBe(0);
+ expect(reduxStore.getState().check).toBe(false);
+
+ reduxStore.dispatch({type: 'ADD'});
+ reduxStore.dispatch({type: 'TOGGLE'});
+
+ expect(reduxStore.getState().counter).toBe(1);
+ expect(reduxStore.getState().check).toBe(true);
+ });
+
+ it('Should apply enhancers', async () => {
+ let counter = 0;
+ let middlewareCallList = [];
+
+ const callCounter = store => next => action => {
+ middlewareCallList.push('callCounter');
+ counter++;
+ let result = next(action);
+ return result;
+ }
+
+ const reduxStore = createStore((state, action) => {
+ switch (action.type) {
+ case('toggle'):
+ return {
+ check: !state.check
+ }
+ default:
+ return state;
+ }
+ }, {check: false}, applyMiddleware(callCounter));
+
+ reduxStore.dispatch({type: 'toggle'});
+ reduxStore.dispatch({type: 'toggle'});
+
+ expect(counter).toBe(3); // NOTE: first action is always store initialization
+ });
+
+ it('Should apply multiple enhancers', async () => {
+ let counter = 0;
+ let lastAction = '';
+ let middlewareCallList = [];
+
+ const callCounter = store => next => action => {
+ middlewareCallList.push('callCounter');
+ counter++;
+ let result = next(action);
+ return result;
+ }
+
+ const lastFunctionStorage = store => next => action => {
+ middlewareCallList.push('lastFunctionStorage');
+ lastAction = action.type;
+ let result = next(action);
+ return result;
+ }
+
+ const reduxStore = createStore((state, action) => {
+ switch (action.type) {
+ case('toggle'):
+ return {
+ check: !state.check
+ }
+ default:
+ return state;
+ }
+ }, {check: false}, applyMiddleware(callCounter, lastFunctionStorage));
+
+ reduxStore.dispatch({type: 'toggle'});
+
+ expect(counter).toBe(2); // NOTE: first action is always store initialization
+ expect(lastAction).toBe('toggle');
+ expect(middlewareCallList[0]).toBe("callCounter");
+ expect(middlewareCallList[1]).toBe("lastFunctionStorage");
+ });
+});
diff --git a/scripts/__tests__/HorizonXText/adapters/ReduxAdapterPromiseMiddleware.js b/scripts/__tests__/HorizonXText/adapters/ReduxAdapterPromiseMiddleware.js
new file mode 100644
index 00000000..509706cf
--- /dev/null
+++ b/scripts/__tests__/HorizonXText/adapters/ReduxAdapterPromiseMiddleware.js
@@ -0,0 +1,96 @@
+export const ActionType = {
+ Pending: 'PENDING',
+ Fulfilled: 'FULFILLED',
+ Rejected: 'REJECTED',
+};
+
+export const promise = store => next => action => {
+ //let result = next(action);
+ store._horizonXstore.$queue.dispatch(action);
+ return result;
+};
+
+export function createPromise(config = {}) {
+ const defaultTypes = [ActionType.Pending, ActionType.Fulfilled, ActionType.Rejected];
+ const PROMISE_TYPE_SUFFIXES = config.promiseTypeSuffixes || defaultTypes;
+ const PROMISE_TYPE_DELIMITER = config.promiseTypeDelimiter || '_';
+
+ return store => {
+ const { dispatch } = store;
+
+ return next => action => {
+ /**
+ * Instantiate variables to hold:
+ * (1) the promise
+ * (2) the data for optimistic updates
+ */
+ let promise;
+ let data;
+
+ /**
+ * There are multiple ways to dispatch a promise. The first step is to
+ * determine if the promise is defined:
+ * (a) explicitly (action.payload.promise is the promise)
+ * (b) implicitly (action.payload is the promise)
+ * (c) as an async function (returns a promise when called)
+ *
+ * If the promise is not defined in one of these three ways, we don't do
+ * anything and move on to the next middleware in the middleware chain.
+ */
+
+ // Step 1a: Is there a payload?
+ if (action.payload) {
+ const PAYLOAD = action.payload;
+
+ // Step 1.1: Is the promise implicitly defined?
+ if (isPromise(PAYLOAD)) {
+ promise = PAYLOAD;
+ }
+
+ // Step 1.2: Is the promise explicitly defined?
+ else if (isPromise(PAYLOAD.promise)) {
+ promise = PAYLOAD.promise;
+ data = PAYLOAD.data;
+ }
+
+ // Step 1.3: Is the promise returned by an async function?
+ else if (typeof PAYLOAD === 'function' || typeof PAYLOAD.promise === 'function') {
+ promise = PAYLOAD.promise ? PAYLOAD.promise() : PAYLOAD();
+ data = PAYLOAD.promise ? PAYLOAD.data : undefined;
+
+ // Step 1.3.1: Is the return of action.payload a promise?
+ if (!isPromise(promise)) {
+ // If not, move on to the next middleware.
+ return next({
+ ...action,
+ payload: promise,
+ });
+ }
+ }
+
+ // Step 1.4: If there's no promise, move on to the next middleware.
+ else {
+ return next(action);
+ }
+
+ // Step 1b: If there's no payload, move on to the next middleware.
+ } else {
+ return next(action);
+ }
+
+ /**
+ * Instantiate and define constants for:
+ * (1) the action type
+ * (2) the action meta
+ */
+ const TYPE = action.type;
+ const META = action.meta;
+
+ /**
+ * Instantiate and define constants for the action type suffixes.
+ * These are appended to the end of the action type.
+ */
+ const [PENDING, FULFILLED, REJECTED] = PROMISE_TYPE_SUFFIXES;
+ };
+ };
+}
diff --git a/scripts/__tests__/HorizonXText/adapters/ReduxAdapterThunk.test.js b/scripts/__tests__/HorizonXText/adapters/ReduxAdapterThunk.test.js
new file mode 100644
index 00000000..3785b92c
--- /dev/null
+++ b/scripts/__tests__/HorizonXText/adapters/ReduxAdapterThunk.test.js
@@ -0,0 +1,34 @@
+import { createStore, applyMiddleware, thunk } from '../../../../libs/horizon/src/horizonx/adapters/redux';
+
+describe('Redux thunk', () => {
+ it('should use apply thunk middleware', async () => {
+ const MAX_TODOS = 5;
+
+ function addTodosIfAllowed(todoText) {
+ return (dispatch, getState) => {
+ const state = getState();
+
+ if (state.todos.length < MAX_TODOS) {
+ dispatch({ type: 'ADD_TODO', text: todoText });
+ }
+ };
+ }
+
+ const todoStore = createStore(
+ (state = { todos: [] }, action) => {
+ if (action.type === 'ADD_TODO') {
+ return { todos: state.todos?.concat(action.text) };
+ }
+ return state;
+ },
+ null,
+ applyMiddleware(thunk)
+ );
+
+ for (let i = 0; i < 10; i++) {
+ todoStore.dispatch(addTodosIfAllowed('todo no.' + i));
+ }
+
+ expect(todoStore.getState().todos.length).toBe(5);
+ });
+});
diff --git a/scripts/__tests__/HorizonXText/adapters/ReduxReactAdapter.test.js b/scripts/__tests__/HorizonXText/adapters/ReduxReactAdapter.test.js
new file mode 100644
index 00000000..5977671a
--- /dev/null
+++ b/scripts/__tests__/HorizonXText/adapters/ReduxReactAdapter.test.js
@@ -0,0 +1,358 @@
+import horizon, * as Horizon from '@cloudsop/horizon/index.ts';
+import {
+ batch,
+ connect,
+ createStore,
+ Provider,
+ useDispatch,
+ useSelector,
+ useStore,
+ createSelectorHook,
+ createDispatchHook,
+} from '../../../../libs/horizon/src/horizonx/adapters/redux';
+import { triggerClickEvent } from '../../jest/commonComponents';
+
+const BUTTON = 'button';
+const BUTTON2 = 'button2';
+const RESULT = 'result';
+const CONTAINER = 'container';
+
+function getE(id) {
+ return document.getElementById(id);
+}
+
+describe('Redux/React binding adapter', () => {
+ beforeEach(() => {
+ const container = document.createElement('div');
+ container.id = CONTAINER;
+ document.body.appendChild(container);
+ });
+
+ afterEach(() => {
+ document.body.removeChild(getE(CONTAINER));
+ });
+
+ it('Should create provider context', async () => {
+ const reduxStore = createStore((state = 'state', action) => state);
+
+ const Child = () => {
+ const store = useStore();
+ return
{store.getState()}
;
+ };
+
+ const Wrapper = () => {
+ return (
+
+
+
+ );
+ };
+
+ Horizon.render(
, getE(CONTAINER));
+
+ expect(getE(RESULT).innerHTML).toBe('state');
+ });
+
+ it('Should use dispatch', async () => {
+ const reduxStore = createStore((state = 0, action) => {
+ if (action.type === 'ADD') return state + 1;
+ return state;
+ });
+
+ const Child = () => {
+ const store = useStore();
+ const dispatch = useDispatch();
+ return (
+
+
{store.getState()}
+
+
+ );
+ };
+
+ const Wrapper = () => {
+ return (
+
+
+
+ );
+ };
+
+ Horizon.render(
, getE(CONTAINER));
+
+ expect(reduxStore.getState()).toBe(0);
+
+ Horizon.act(() => {
+ triggerClickEvent(getE(CONTAINER), BUTTON);
+ });
+
+ expect(reduxStore.getState()).toBe(1);
+ });
+
+ it('Should use selector', async () => {
+ const reduxStore = createStore((state = 0, action) => {
+ if (action.type === 'ADD') return state + 1;
+ return state;
+ });
+
+ const Child = () => {
+ const count = useSelector(state => state);
+ const dispatch = useDispatch();
+ return (
+
+
{count}
+
+
+ );
+ };
+
+ const Wrapper = () => {
+ return (
+
+
+
+ );
+ };
+
+ Horizon.render(
, getE(CONTAINER));
+
+ expect(getE(RESULT).innerHTML).toBe('0');
+
+ Horizon.act(() => {
+ triggerClickEvent(getE(CONTAINER), BUTTON);
+ triggerClickEvent(getE(CONTAINER), BUTTON);
+ });
+
+ expect(getE(RESULT).innerHTML).toBe('2');
+ });
+
+ it('Should use connect', async () => {
+ const reduxStore = createStore(
+ (state, action) => {
+ switch (action.type) {
+ case 'INCREMENT':
+ return {
+ ...state,
+ value: state.negative ? state.value - action.amount : state.value + action.amount,
+ };
+ case 'TOGGLE':
+ return {
+ ...state,
+ negative: !state.negative,
+ };
+ default:
+ return state;
+ }
+ },
+ { negative: false, value: 0 }
+ );
+
+ const Child = connect(
+ (state, ownProps) => {
+ // map state to props
+ return { ...state, ...ownProps };
+ },
+ (dispatch, ownProps) => {
+ // map dispatch to props
+ return {
+ increment: () => dispatch({ type: 'INCREMENT', amount: ownProps.amount }),
+ };
+ },
+ (stateProps, dispatchProps, ownProps) => {
+ //merge props
+ return { stateProps, dispatchProps, ownProps };
+ },
+ {}
+ )(props => {
+ const n = props.stateProps.negative;
+ return (
+
+
+ {n ? '-' : '+'}
+ {props.stateProps.value}
+
+
+
+ );
+ });
+
+ const Wrapper = () => {
+ const [amount, setAmount] = Horizon.useState(5);
+ return (
+
+
+
+
+ );
+ };
+
+ Horizon.render(
, getE(CONTAINER));
+
+ expect(getE(RESULT).innerHTML).toBe('+0');
+
+ Horizon.act(() => {
+ triggerClickEvent(getE(CONTAINER), BUTTON);
+ });
+
+ expect(getE(RESULT).innerHTML).toBe('+5');
+
+ Horizon.act(() => {
+ triggerClickEvent(getE(CONTAINER), BUTTON2);
+ });
+
+ Horizon.act(() => {
+ triggerClickEvent(getE(CONTAINER), BUTTON);
+ });
+
+ expect(getE(RESULT).innerHTML).toBe('+8');
+ });
+
+ it('Should batch dispatches', async () => {
+ const reduxStore = createStore((state = 0, action) => {
+ if (action.type == 'ADD') return state + 1;
+ return state;
+ });
+
+ let renderCounter = 0;
+
+ function Counter() {
+ renderCounter++;
+
+ const value = useSelector(state => state);
+ const dispatch = useDispatch();
+
+ return (
+
+
{value}
+
+
+ );
+ }
+
+ Horizon.render(
+
+
+ ,
+ getE(CONTAINER)
+ );
+
+ expect(getE(RESULT).innerHTML).toBe('0');
+ expect(renderCounter).toBe(1);
+
+ Horizon.act(() => {
+ triggerClickEvent(getE(CONTAINER), BUTTON);
+ });
+
+ expect(getE(RESULT).innerHTML).toBe('10');
+ expect(renderCounter).toBe(2);
+ });
+
+ it('Should use multiple contexts', async () => {
+ const counterStore = createStore((state = 0, action) => {
+ if (action.type === 'ADD') return state + 1;
+ return state;
+ });
+
+ const toggleStore = createStore((state = false, action) => {
+ if (action.type === 'TOGGLE') return !state;
+ return state;
+ });
+
+ const counterContext = horizon.createContext();
+ const toggleContext = horizon.createContext();
+
+ function Counter() {
+ const count = createSelectorHook(counterContext)();
+ const dispatch = createDispatchHook(counterContext)();
+
+ return (
+
+ );
+ }
+
+ function Toggle() {
+ const check = createSelectorHook(toggleContext)();
+ const dispatch = createDispatchHook(toggleContext)();
+
+ return (
+
+ );
+ }
+
+ function Wrapper() {
+ return (
+
+ );
+ }
+
+ Horizon.render(
, getE(CONTAINER));
+
+ expect(getE(BUTTON).innerHTML).toBe('0');
+ expect(getE(BUTTON2).innerHTML).toBe('false');
+
+ Horizon.act(() => {
+ triggerClickEvent(getE(CONTAINER), BUTTON);
+ triggerClickEvent(getE(CONTAINER), BUTTON2);
+ });
+
+ expect(getE(BUTTON).innerHTML).toBe('1');
+ expect(getE(BUTTON2).innerHTML).toBe('true');
+ });
+});
diff --git a/scripts/__tests__/HorizonXText/class/ClassException.test.js b/scripts/__tests__/HorizonXText/class/ClassException.test.js
new file mode 100644
index 00000000..64cea5aa
--- /dev/null
+++ b/scripts/__tests__/HorizonXText/class/ClassException.test.js
@@ -0,0 +1,69 @@
+import * as Horizon from '@cloudsop/horizon/index.ts';
+import { clearStore, createStore, useStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler';
+import { Text } from '../../jest/commonComponents';
+
+describe('测试 Class VNode 清除时,对引用清除', () => {
+ const { unmountComponentAtNode } = Horizon;
+ let container = null;
+ let globalState = {
+ name: 'bing dun dun',
+ isWin: true,
+ isShow: true,
+ };
+
+ beforeEach(() => {
+ // 创建一个 DOM 元素作为渲染目标
+ container = document.createElement('div');
+ document.body.appendChild(container);
+
+ createStore({
+ id: 'user',
+ state: globalState,
+ actions: {
+ setWin: (state, val) => {
+ state.isWin = val;
+ },
+ hide: state => {
+ state.isShow = false;
+ },
+ updateName: (state, val) => {
+ state.name = val;
+ },
+ },
+ });
+ });
+
+ afterEach(() => {
+ // 退出时进行清理
+ unmountComponentAtNode(container);
+ container.remove();
+ container = null;
+
+ clearStore('user');
+ });
+
+ it('test observer.clearByNode', () => {
+ class Child extends Horizon.Component {
+ userStore = useStore('user');
+
+ render() {
+ // Do not modify the store data in the render method. Otherwise, an infinite loop may occur.
+ this.userStore.updateName(this.userStore.name === 'bing dun dun' ? 'huo dun dun' : 'bing dun dun');
+
+ return (
+
+
+
+
+ );
+ }
+ }
+
+ expect(() => {
+ Horizon.render(
, container);
+ }).toThrow(
+ 'The number of updates exceeds the upper limit 50.\n' +
+ ' A component maybe repeatedly invokes setState on componentWillUpdate or componentDidUpdate.'
+ );
+ });
+});
diff --git a/scripts/__tests__/HorizonXText/class/ClassStateArray.test.js b/scripts/__tests__/HorizonXText/class/ClassStateArray.test.js
new file mode 100644
index 00000000..c61aed9c
--- /dev/null
+++ b/scripts/__tests__/HorizonXText/class/ClassStateArray.test.js
@@ -0,0 +1,220 @@
+import * as Horizon from '@cloudsop/horizon/index.ts';
+import { clearStore, createStore, useStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler';
+import { App, Text, triggerClickEvent } from '../../jest/commonComponents';
+
+describe('在Class组件中,测试store中的Array', () => {
+ const { unmountComponentAtNode } = Horizon;
+ let container = null;
+ beforeEach(() => {
+ // 创建一个 DOM 元素作为渲染目标
+ container = document.createElement('div');
+ document.body.appendChild(container);
+
+ const persons = [
+ { name: 'p1', age: 1 },
+ { name: 'p2', age: 2 },
+ ];
+
+ createStore({
+ id: 'user',
+ state: {
+ type: 'bing dun dun',
+ persons: persons,
+ },
+ actions: {
+ addOnePerson: (state, person) => {
+ state.persons.push(person);
+ },
+ delOnePerson: state => {
+ state.persons.pop();
+ },
+ clearPersons: state => {
+ state.persons = null;
+ },
+ },
+ });
+ });
+
+ afterEach(() => {
+ // 退出时进行清理
+ unmountComponentAtNode(container);
+ container.remove();
+ container = null;
+
+ clearStore('user');
+ });
+
+ const newPerson = { name: 'p3', age: 3 };
+
+ class Parent extends Horizon.Component {
+ userStore = useStore('user');
+
+ addOnePerson = () => {
+ this.userStore.addOnePerson(newPerson);
+ };
+
+ delOnePerson = () => {
+ this.userStore.delOnePerson();
+ };
+
+ render() {
+ return (
+
+
+
+
{this.props.children}
+
+ );
+ }
+ }
+
+ it('测试Array方法: push()、pop()', () => {
+ class Child extends Horizon.Component {
+ userStore = useStore('user');
+
+ render() {
+ return (
+
+
+
+ );
+ }
+ }
+
+ Horizon.render(
, container);
+
+ expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: 2');
+ // 在Array中增加一个对象
+ Horizon.act(() => {
+ triggerClickEvent(container, 'addBtn');
+ });
+ expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: 3');
+
+ // 在Array中删除一个对象
+ Horizon.act(() => {
+ triggerClickEvent(container, 'delBtn');
+ });
+ expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: 2');
+ });
+
+ it('测试Array方法: entries()、push()、shift()、unshift、直接赋值', () => {
+ let globalStore = null;
+
+ class Child extends Horizon.Component {
+ userStore = useStore('user');
+
+ constructor(props) {
+ super(props);
+ globalStore = this.userStore;
+ }
+
+ render() {
+ const nameList = [];
+ const entries = this.userStore.$state.persons?.entries();
+ if (entries) {
+ for (const entry of entries) {
+ nameList.push(entry[1].name);
+ }
+ }
+
+ return (
+
+
+
+ );
+ }
+ }
+
+ Horizon.render(
, container);
+
+ expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2');
+ // push
+ globalStore.$state.persons.push(newPerson);
+ expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3');
+
+ // shift
+ globalStore.$state.persons.shift({ name: 'p0', age: 0 });
+ expect(container.querySelector('#nameList').innerHTML).toBe('name list: p2 p3');
+
+ // 赋值[2]
+ globalStore.$state.persons[2] = { name: 'p4', age: 4 };
+ expect(container.querySelector('#nameList').innerHTML).toBe('name list: p2 p3 p4');
+
+ // 重新赋值[2]
+ globalStore.$state.persons[2] = { name: 'p5', age: 5 };
+ expect(container.querySelector('#nameList').innerHTML).toBe('name list: p2 p3 p5');
+
+ // unshift
+ globalStore.$state.persons.unshift({ name: 'p1', age: 1 });
+ expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3 p5');
+
+ // 重新赋值 null
+ globalStore.$state.persons = null;
+ expect(container.querySelector('#nameList').innerHTML).toBe('name list: ');
+
+ // 重新赋值 [{ name: 'p1', age: 1 }]
+ globalStore.$state.persons = [{ name: 'p1', age: 1 }];
+ expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1');
+ });
+
+ it('测试Array方法: forEach()', () => {
+ let globalStore = null;
+
+ class Child extends Horizon.Component {
+ userStore = useStore('user');
+
+ constructor(props) {
+ super(props);
+ globalStore = this.userStore;
+ }
+
+ render() {
+ const nameList = [];
+ this.userStore.$state.persons?.forEach(per => {
+ nameList.push(per.name);
+ });
+
+ return (
+
+
+
+ );
+ }
+ }
+
+ Horizon.render(
, container);
+
+ expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2');
+ // push
+ globalStore.$state.persons.push(newPerson);
+ expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3');
+
+ // shift
+ globalStore.$state.persons.shift({ name: 'p0', age: 0 });
+ expect(container.querySelector('#nameList').innerHTML).toBe('name list: p2 p3');
+
+ // 赋值[2]
+ globalStore.$state.persons[2] = { name: 'p4', age: 4 };
+ expect(container.querySelector('#nameList').innerHTML).toBe('name list: p2 p3 p4');
+
+ // 重新赋值[2]
+ globalStore.$state.persons[2] = { name: 'p5', age: 5 };
+ expect(container.querySelector('#nameList').innerHTML).toBe('name list: p2 p3 p5');
+
+ // unshift
+ globalStore.$state.persons.unshift({ name: 'p1', age: 1 });
+ expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3 p5');
+
+ // 重新赋值 null
+ globalStore.$state.persons = null;
+ expect(container.querySelector('#nameList').innerHTML).toBe('name list: ');
+
+ // 重新赋值 [{ name: 'p1', age: 1 }]
+ globalStore.$state.persons = [{ name: 'p1', age: 1 }];
+ expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1');
+ });
+});
diff --git a/scripts/__tests__/HorizonXText/class/ClassStateMap.test.js b/scripts/__tests__/HorizonXText/class/ClassStateMap.test.js
new file mode 100644
index 00000000..071ad650
--- /dev/null
+++ b/scripts/__tests__/HorizonXText/class/ClassStateMap.test.js
@@ -0,0 +1,340 @@
+import * as Horizon from '@cloudsop/horizon/index.ts';
+import { clearStore, createStore, useStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler';
+import { App, Text, triggerClickEvent } from '../../jest/commonComponents';
+
+describe('在Class组件中,测试store中的Map', () => {
+ const { unmountComponentAtNode } = Horizon;
+ let container = null;
+ beforeEach(() => {
+ // 创建一个 DOM 元素作为渲染目标
+ container = document.createElement('div');
+ document.body.appendChild(container);
+
+ const persons = new Map([
+ ['p1', 1],
+ ['p2', 2],
+ ]);
+
+ createStore({
+ id: 'user',
+ state: {
+ type: 'bing dun dun',
+ persons: persons,
+ },
+ actions: {
+ addOnePerson: (state, person) => {
+ state.persons.set(person.name, person.age);
+ },
+ delOnePerson: (state, person) => {
+ state.persons.delete(person.name);
+ },
+ clearPersons: state => {
+ state.persons.clear();
+ },
+ },
+ });
+ });
+
+ afterEach(() => {
+ // 退出时进行清理
+ unmountComponentAtNode(container);
+ container.remove();
+ container = null;
+
+ clearStore('user');
+ });
+
+ const newPerson = { name: 'p3', age: 3 };
+
+ class Parent extends Horizon.Component {
+ userStore = useStore('user');
+
+ addOnePerson = () => {
+ this.userStore.addOnePerson(newPerson);
+ };
+ delOnePerson = () => {
+ this.userStore.delOnePerson(newPerson);
+ };
+ clearPersons = () => {
+ this.userStore.clearPersons();
+ };
+
+ render() {
+ return (
+
+
+
+
+
{this.props.children}
+
+ );
+ }
+ }
+
+ it('测试Map方法: set()、delete()、clear()', () => {
+ class Child extends Horizon.Component {
+ userStore = useStore('user');
+
+ render() {
+ return (
+
+
+
+ );
+ }
+ }
+
+ Horizon.render(
, container);
+
+ expect(container.querySelector('#size').innerHTML).toBe('persons number: 2');
+ // 在Map中增加一个对象
+ Horizon.act(() => {
+ triggerClickEvent(container, 'addBtn');
+ });
+ expect(container.querySelector('#size').innerHTML).toBe('persons number: 3');
+
+ // 在Map中删除一个对象
+ Horizon.act(() => {
+ triggerClickEvent(container, 'delBtn');
+ });
+ expect(container.querySelector('#size').innerHTML).toBe('persons number: 2');
+
+ // clear Map
+ Horizon.act(() => {
+ triggerClickEvent(container, 'clearBtn');
+ });
+ expect(container.querySelector('#size').innerHTML).toBe('persons number: 0');
+ });
+
+ it('测试Map方法: keys()', () => {
+ class Child extends Horizon.Component {
+ userStore = useStore('user');
+
+ render() {
+ const nameList = [];
+ const keys = this.userStore.$state.persons.keys();
+ for (const key of keys) {
+ nameList.push(key);
+ }
+
+ return (
+
+
+
+ );
+ }
+ }
+
+ Horizon.render(
, container);
+
+ expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2');
+ // 在Map中增加一个对象
+ Horizon.act(() => {
+ triggerClickEvent(container, 'addBtn');
+ });
+ expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3');
+
+ // 在Map中删除一个对象
+ Horizon.act(() => {
+ triggerClickEvent(container, 'delBtn');
+ });
+ expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2');
+
+ // clear Map
+ Horizon.act(() => {
+ triggerClickEvent(container, 'clearBtn');
+ });
+ expect(container.querySelector('#nameList').innerHTML).toBe('name list: ');
+ });
+
+ it('测试Map方法: values()', () => {
+ class Child extends Horizon.Component {
+ userStore = useStore('user');
+
+ render() {
+ const ageList = [];
+ const values = this.userStore.$state.persons.values();
+ for (const val of values) {
+ ageList.push(val);
+ }
+
+ return (
+
+
+
+ );
+ }
+ }
+
+ Horizon.render(
, container);
+
+ expect(container.querySelector('#ageList').innerHTML).toBe('age list: 1 2');
+ // 在Map中增加一个对象
+ Horizon.act(() => {
+ triggerClickEvent(container, 'addBtn');
+ });
+ expect(container.querySelector('#ageList').innerHTML).toBe('age list: 1 2 3');
+
+ // 在Map中删除一个对象
+ Horizon.act(() => {
+ triggerClickEvent(container, 'delBtn');
+ });
+ expect(container.querySelector('#ageList').innerHTML).toBe('age list: 1 2');
+
+ // clear Map
+ Horizon.act(() => {
+ triggerClickEvent(container, 'clearBtn');
+ });
+ expect(container.querySelector('#ageList').innerHTML).toBe('age list: ');
+ });
+
+ it('测试Map方法: entries()', () => {
+ class Child extends Horizon.Component {
+ userStore = useStore('user');
+
+ render() {
+ const nameList = [];
+ const entries = this.userStore.$state.persons.entries();
+ for (const entry of entries) {
+ nameList.push(entry[0]);
+ }
+
+ return (
+
+
+
+ );
+ }
+ }
+
+ Horizon.render(
, container);
+
+ expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2');
+ // 在Map中增加一个对象
+ Horizon.act(() => {
+ triggerClickEvent(container, 'addBtn');
+ });
+ expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3');
+
+ // 在Map中删除一个对象
+ Horizon.act(() => {
+ triggerClickEvent(container, 'delBtn');
+ });
+ expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2');
+
+ // clear Map
+ Horizon.act(() => {
+ triggerClickEvent(container, 'clearBtn');
+ });
+ expect(container.querySelector('#nameList').innerHTML).toBe('name list: ');
+ });
+
+ it('测试Map方法: forEach()', () => {
+ class Child extends Horizon.Component {
+ userStore = useStore('user');
+
+ render() {
+ const nameList = [];
+ this.userStore.$state.persons.forEach((val, key) => {
+ nameList.push(key);
+ });
+
+ return (
+
+
+
+ );
+ }
+ }
+
+ Horizon.render(
, container);
+
+ expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2');
+ // 在Map中增加一个对象
+ Horizon.act(() => {
+ triggerClickEvent(container, 'addBtn');
+ });
+ expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3');
+
+ // 在Map中删除一个对象
+ Horizon.act(() => {
+ triggerClickEvent(container, 'delBtn');
+ });
+ expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2');
+
+ // clear Map
+ Horizon.act(() => {
+ triggerClickEvent(container, 'clearBtn');
+ });
+ expect(container.querySelector('#nameList').innerHTML).toBe('name list: ');
+ });
+
+ it('测试Map方法: has()', () => {
+ class Child extends Horizon.Component {
+ userStore = useStore('user');
+
+ render() {
+ return (
+
+
+
+ );
+ }
+ }
+
+ Horizon.render(
, container);
+
+ expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: false');
+ // 在Map中增加一个对象
+ Horizon.act(() => {
+ triggerClickEvent(container, 'addBtn');
+ });
+ expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: true');
+ });
+
+ it('测试Map方法: for of()', () => {
+ class Child extends Horizon.Component {
+ userStore = useStore('user');
+
+ render() {
+ const nameList = [];
+ for (const per of this.userStore.$state.persons) {
+ nameList.push(per[0]);
+ }
+
+ return (
+
+
+
+ );
+ }
+ }
+
+ Horizon.render(
, container);
+
+ expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2');
+ // 在Map中增加一个对象
+ Horizon.act(() => {
+ triggerClickEvent(container, 'addBtn');
+ });
+ expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3');
+
+ // 在Map中删除一个对象
+ Horizon.act(() => {
+ triggerClickEvent(container, 'delBtn');
+ });
+ expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2');
+
+ // clear Map
+ Horizon.act(() => {
+ triggerClickEvent(container, 'clearBtn');
+ });
+ expect(container.querySelector('#nameList').innerHTML).toBe('name list: ');
+ });
+});
diff --git a/scripts/__tests__/HorizonXText/clear/ClassVNodeClear.test.js b/scripts/__tests__/HorizonXText/clear/ClassVNodeClear.test.js
new file mode 100644
index 00000000..a2226b1c
--- /dev/null
+++ b/scripts/__tests__/HorizonXText/clear/ClassVNodeClear.test.js
@@ -0,0 +1,119 @@
+import * as Horizon from '@cloudsop/horizon/index.ts';
+import { clearStore, createStore, useStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler';
+import { Text, triggerClickEvent } from '../../jest/commonComponents';
+import { getObserver } from '../../../../libs/horizon/src/horizonx/proxy/ProxyHandler';
+
+describe('测试 Class VNode 清除时,对引用清除', () => {
+ const { unmountComponentAtNode } = Horizon;
+ let container = null;
+ let globalState = {
+ name: 'bing dun dun',
+ isWin: true,
+ isShow: true,
+ };
+
+ beforeEach(() => {
+ // 创建一个 DOM 元素作为渲染目标
+ container = document.createElement('div');
+ document.body.appendChild(container);
+
+ createStore({
+ id: 'user',
+ state: globalState,
+ actions: {
+ setWin: (state, val) => {
+ state.isWin = val;
+ },
+ hide: state => {
+ state.isShow = false;
+ },
+ updateName: (state, val) => {
+ state.name = val;
+ },
+ },
+ });
+ });
+
+ afterEach(() => {
+ // 退出时进行清理
+ unmountComponentAtNode(container);
+ container.remove();
+ container = null;
+
+ clearStore('user');
+ });
+
+ it('test observer.clearByNode', () => {
+ class App extends Horizon.Component {
+ userStore = useStore('user');
+
+ render() {
+ return (
+
+
+ {this.userStore.isShow &&
}
+
+ );
+ }
+ }
+
+ class Parent extends Horizon.Component {
+ userStore = useStore('user');
+
+ setWin = () => {
+ this.userStore.setWin(!this.userStore.isWin);
+ };
+
+ render() {
+ return (
+
+
+ {this.userStore.isWin && }
+
+ );
+ }
+ }
+
+ class Child extends Horizon.Component {
+ userStore = useStore('user');
+
+ render() {
+ // this.userStore.updateName(this.userStore.name === 'bing dun dun' ? 'huo dun dun' : 'bing dun dun');
+
+ return (
+
+
+
+
+ );
+ }
+ }
+
+ Horizon.render(
, container);
+
+ // Parent and Child hold the isWin key
+ expect(getObserver(globalState).keyVNodes.get('isWin').size).toBe(2);
+
+ Horizon.act(() => {
+ triggerClickEvent(container, 'toggleBtn');
+ });
+ // Parent hold the isWin key
+ expect(getObserver(globalState).keyVNodes.get('isWin').size).toBe(1);
+
+ Horizon.act(() => {
+ triggerClickEvent(container, 'toggleBtn');
+ });
+ // Parent and Child hold the isWin key
+ expect(getObserver(globalState).keyVNodes.get('isWin').size).toBe(2);
+
+ Horizon.act(() => {
+ triggerClickEvent(container, 'hideBtn');
+ });
+ // no component hold the isWin key
+ expect(getObserver(globalState).keyVNodes.get('isWin')).toBe(undefined);
+ });
+});
diff --git a/scripts/__tests__/HorizonXText/clear/FunctionVNodeClear.test.js b/scripts/__tests__/HorizonXText/clear/FunctionVNodeClear.test.js
new file mode 100644
index 00000000..ead0453a
--- /dev/null
+++ b/scripts/__tests__/HorizonXText/clear/FunctionVNodeClear.test.js
@@ -0,0 +1,114 @@
+import * as Horizon from '@cloudsop/horizon/index.ts';
+import { clearStore, createStore, useStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler';
+import { Text, triggerClickEvent } from '../../jest/commonComponents';
+import { getObserver } from '../../../../libs/horizon/src/horizonx/proxy/ProxyHandler';
+
+describe('测试VNode清除时,对引用清除', () => {
+ const { unmountComponentAtNode } = Horizon;
+ let container = null;
+ let globalState = {
+ name: 'bing dun dun',
+ isWin: true,
+ isShow: true,
+ };
+
+ beforeEach(() => {
+ // 创建一个 DOM 元素作为渲染目标
+ container = document.createElement('div');
+ document.body.appendChild(container);
+
+ createStore({
+ id: 'user',
+ state: globalState,
+ actions: {
+ setWin: (state, val) => {
+ state.isWin = val;
+ },
+ hide: state => {
+ state.isShow = false;
+ },
+ },
+ });
+ });
+
+ afterEach(() => {
+ // 退出时进行清理
+ unmountComponentAtNode(container);
+ container.remove();
+ container = null;
+
+ clearStore('user');
+ });
+
+ it('test observer.clearByNode', () => {
+ class App extends Horizon.Component {
+ userStore = useStore('user');
+
+ render() {
+ return (
+
+
+ {this.userStore.isShow &&
}
+
+ );
+ }
+ }
+
+ class Parent extends Horizon.Component {
+ userStore = useStore('user');
+
+ setWin = () => {
+ this.userStore.setWin(!this.userStore.isWin);
+ };
+
+ render() {
+ return (
+
+
+ {this.userStore.isWin && }
+
+ );
+ }
+ }
+
+ class Child extends Horizon.Component {
+ userStore = useStore('user');
+
+ render() {
+ return (
+
+
+
+
+ );
+ }
+ }
+
+ Horizon.render(
, container);
+
+ // Parent and Child hold the isWin key
+ expect(getObserver(globalState).keyVNodes.get('isWin').size).toBe(2);
+
+ Horizon.act(() => {
+ triggerClickEvent(container, 'toggleBtn');
+ });
+ // Parent hold the isWin key
+ expect(getObserver(globalState).keyVNodes.get('isWin').size).toBe(1);
+
+ Horizon.act(() => {
+ triggerClickEvent(container, 'toggleBtn');
+ });
+ // Parent and Child hold the isWin key
+ expect(getObserver(globalState).keyVNodes.get('isWin').size).toBe(2);
+
+ Horizon.act(() => {
+ triggerClickEvent(container, 'hideBtn');
+ });
+ // no component hold the isWin key
+ expect(getObserver(globalState).keyVNodes.get('isWin')).toBe(undefined);
+ });
+});
diff --git a/scripts/__tests__/HorizonXText/edgeCases/proxy.test.js b/scripts/__tests__/HorizonXText/edgeCases/proxy.test.js
new file mode 100644
index 00000000..a643f311
--- /dev/null
+++ b/scripts/__tests__/HorizonXText/edgeCases/proxy.test.js
@@ -0,0 +1,21 @@
+import { createProxy } from '../../../../libs/horizon/src/horizonx/proxy/ProxyHandler';
+
+describe('Proxy', () => {
+ const arr = [];
+
+ it('Should not double wrap proxies', async () => {
+ const proxy1 = createProxy(arr);
+
+ const proxy2 = createProxy(proxy1);
+
+ expect(proxy1 === proxy2).toBe(true);
+ });
+
+ it('Should re-use existing proxy of same object', async () => {
+ const proxy1 = createProxy(arr);
+
+ const proxy2 = createProxy(arr);
+
+ expect(proxy1 === proxy2).toBe(true);
+ });
+});
diff --git a/scripts/__tests__/jest/commonComponents.js b/scripts/__tests__/jest/commonComponents.js
index 6abda0f8..4823b9b7 100644
--- a/scripts/__tests__/jest/commonComponents.js
+++ b/scripts/__tests__/jest/commonComponents.js
@@ -1,9 +1,8 @@
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import * as Horizon from '@cloudsop/horizon/index.ts';
import { getLogUtils } from './testUtils';
-export const App = (props) => {
+export const App = props => {
const Parent = props.parent;
const Child = props.child;
@@ -16,8 +15,15 @@ export const App = (props) => {
);
};
-export const Text = (props) => {
- const LogUtils =getLogUtils();
+export const Text = props => {
+ const LogUtils = getLogUtils();
LogUtils.log(props.text);
return
{props.text}
;
};
+
+export function triggerClickEvent(container, id) {
+ const event = new MouseEvent('click', {
+ bubbles: true,
+ });
+ container.querySelector(`#${id}`).dispatchEvent(event);
+}
diff --git a/scripts/__tests__/jest/testUtils.js b/scripts/__tests__/jest/testUtils.js
index 6d4b7ac2..65cea65e 100755
--- a/scripts/__tests__/jest/testUtils.js
+++ b/scripts/__tests__/jest/testUtils.js
@@ -1,4 +1,4 @@
-import { allDelegatedNativeEvents } from '../../../libs/horizon/src/event/EventCollection';
+import { allDelegatedNativeEvents } from '@cloudsop/horizon/src/event/EventHub';
//import * as LogUtils from './logUtils';
export const stopBubbleOrCapture = (e, value) => {
@@ -107,4 +107,4 @@ export function getLogUtils() {
logger = new LogUtils();
}
return logger;
-}
\ No newline at end of file
+}
diff --git a/scripts/rollup/rollup.config.js b/scripts/rollup/rollup.config.js
index fb40e560..c3c31b49 100644
--- a/scripts/rollup/rollup.config.js
+++ b/scripts/rollup/rollup.config.js
@@ -1,6 +1,7 @@
import nodeResolve from '@rollup/plugin-node-resolve';
import babel from '@rollup/plugin-babel';
import path from 'path';
+import fs from 'fs';
import replace from '@rollup/plugin-replace';
import copy from './copy-plugin';
import { terser } from 'rollup-plugin-terser';
@@ -11,6 +12,14 @@ const extensions = ['.js', '.ts'];
const libDir = path.join(__dirname, '../../libs/horizon');
const rootDir = path.join(__dirname, '../..');
const outDir = path.join(rootDir, 'build', 'horizon');
+
+if (!fs.existsSync(path.join(rootDir, 'build'))) {
+ fs.mkdirSync(path.join(rootDir, 'build'));
+}
+if (!fs.existsSync(outDir)) {
+ fs.mkdirSync(outDir);
+}
+
const outputResolve = (...p) => path.resolve(outDir, ...p);
function genConfig(mode) {
@@ -52,7 +61,7 @@ function genConfig(mode) {
mode === 'production' && terser(),
copy([
{
- from: path.join(libDir, 'index.js'),
+ from: path.join(libDir, '/npm/index.js'),
to: path.join(outDir, 'index.js'),
},
{
diff --git a/scripts/template.ejs b/scripts/template.ejs
index 3f426b7e..e2cb0e64 100644
--- a/scripts/template.ejs
+++ b/scripts/template.ejs
@@ -762,3 +762,7 @@ if(!window["moment"]) {
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):e.moment=t()}(this,function(){"use strict";var e,i;function f(){return e.apply(null,arguments)}function o(e){return e instanceof Array||"[object Array]"===Object.prototype.toString.call(e)}function u(e){return null!=e&&"[object Object]"===Object.prototype.toString.call(e)}function m(e,t){return Object.prototype.hasOwnProperty.call(e,t)}function l(e){if(Object.getOwnPropertyNames)return 0===Object.getOwnPropertyNames(e).length;for(var t in e)if(m(e,t))return;return 1}function r(e){return void 0===e}function h(e){return"number"==typeof e||"[object Number]"===Object.prototype.toString.call(e)}function a(e){return e instanceof Date||"[object Date]"===Object.prototype.toString.call(e)}function d(e,t){for(var n=[],s=0;s
>>0,s=0;sFe(e)?(r=e+1,a-Fe(e)):(r=e,a);return{year:r,dayOfYear:o}}function Ae(e,t,n){var s,i,r=Ge(e.year(),t,n),a=Math.floor((e.dayOfYear()-r-1)/7)+1;return a<1?s=a+je(i=e.year()-1,t,n):a>je(e.year(),t,n)?(s=a-je(e.year(),t,n),i=e.year()+1):(i=e.year(),s=a),{week:s,year:i}}function je(e,t,n){var s=Ge(e,t,n),i=Ge(e+1,t,n);return(Fe(e)-s+i)/7}C("w",["ww",2],"wo","week"),C("W",["WW",2],"Wo","isoWeek"),L("week","w"),L("isoWeek","W"),A("week",5),A("isoWeek",5),ce("w",te),ce("ww",te,Q),ce("W",te),ce("WW",te,Q),ge(["w","ww","W","WW"],function(e,t,n,s){t[s.substr(0,1)]=Z(e)});function Ie(e,t){return e.slice(t,7).concat(e.slice(0,t))}C("d",0,"do","day"),C("dd",0,0,function(e){return this.localeData().weekdaysMin(this,e)}),C("ddd",0,0,function(e){return this.localeData().weekdaysShort(this,e)}),C("dddd",0,0,function(e){return this.localeData().weekdays(this,e)}),C("e",0,0,"weekday"),C("E",0,0,"isoWeekday"),L("day","d"),L("weekday","e"),L("isoWeekday","E"),A("day",11),A("weekday",11),A("isoWeekday",11),ce("d",te),ce("e",te),ce("E",te),ce("dd",function(e,t){return t.weekdaysMinRegex(e)}),ce("ddd",function(e,t){return t.weekdaysShortRegex(e)}),ce("dddd",function(e,t){return t.weekdaysRegex(e)}),ge(["dd","ddd","dddd"],function(e,t,n,s){var i=n._locale.weekdaysParse(e,s,n._strict);null!=i?t.d=i:y(n).invalidWeekday=e}),ge(["d","e","E"],function(e,t,n,s){t[s]=Z(e)});var Ze="Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),ze="Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),$e="Su_Mo_Tu_We_Th_Fr_Sa".split("_"),qe=de,Be=de,Je=de;function Qe(){function e(e,t){return t.length-e.length}for(var t,n,s,i,r=[],a=[],o=[],u=[],l=0;l<7;l++)t=_([2e3,1]).day(l),n=me(this.weekdaysMin(t,"")),s=me(this.weekdaysShort(t,"")),i=me(this.weekdays(t,"")),r.push(n),a.push(s),o.push(i),u.push(n),u.push(s),u.push(i);r.sort(e),a.sort(e),o.sort(e),u.sort(e),this._weekdaysRegex=new RegExp("^("+u.join("|")+")","i"),this._weekdaysShortRegex=this._weekdaysRegex,this._weekdaysMinRegex=this._weekdaysRegex,this._weekdaysStrictRegex=new RegExp("^("+o.join("|")+")","i"),this._weekdaysShortStrictRegex=new RegExp("^("+a.join("|")+")","i"),this._weekdaysMinStrictRegex=new RegExp("^("+r.join("|")+")","i")}function Xe(){return this.hours()%12||12}function Ke(e,t){C(e,0,0,function(){return this.localeData().meridiem(this.hours(),this.minutes(),t)})}function et(e,t){return t._meridiemParse}C("H",["HH",2],0,"hour"),C("h",["hh",2],0,Xe),C("k",["kk",2],0,function(){return this.hours()||24}),C("hmm",0,0,function(){return""+Xe.apply(this)+T(this.minutes(),2)}),C("hmmss",0,0,function(){return""+Xe.apply(this)+T(this.minutes(),2)+T(this.seconds(),2)}),C("Hmm",0,0,function(){return""+this.hours()+T(this.minutes(),2)}),C("Hmmss",0,0,function(){return""+this.hours()+T(this.minutes(),2)+T(this.seconds(),2)}),Ke("a",!0),Ke("A",!1),L("hour","h"),A("hour",13),ce("a",et),ce("A",et),ce("H",te),ce("h",te),ce("k",te),ce("HH",te,Q),ce("hh",te,Q),ce("kk",te,Q),ce("hmm",ne),ce("hmmss",se),ce("Hmm",ne),ce("Hmmss",se),ye(["H","HH"],Me),ye(["k","kk"],function(e,t,n){var s=Z(e);t[Me]=24===s?0:s}),ye(["a","A"],function(e,t,n){n._isPm=n._locale.isPM(e),n._meridiem=e}),ye(["h","hh"],function(e,t,n){t[Me]=Z(e),y(n).bigHour=!0}),ye("hmm",function(e,t,n){var s=e.length-2;t[Me]=Z(e.substr(0,s)),t[De]=Z(e.substr(s)),y(n).bigHour=!0}),ye("hmmss",function(e,t,n){var s=e.length-4,i=e.length-2;t[Me]=Z(e.substr(0,s)),t[De]=Z(e.substr(s,2)),t[Se]=Z(e.substr(i)),y(n).bigHour=!0}),ye("Hmm",function(e,t,n){var s=e.length-2;t[Me]=Z(e.substr(0,s)),t[De]=Z(e.substr(s))}),ye("Hmmss",function(e,t,n){var s=e.length-4,i=e.length-2;t[Me]=Z(e.substr(0,s)),t[De]=Z(e.substr(s,2)),t[Se]=Z(e.substr(i))});var tt=z("Hours",!0);var nt,st={calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},longDateFormat:{LTS:"h:mm:ss A",LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D, YYYY",LLL:"MMMM D, YYYY h:mm A",LLLL:"dddd, MMMM D, YYYY h:mm A"},invalidDate:"Invalid date",ordinal:"%d",dayOfMonthOrdinalParse:/\d{1,2}/,relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",ss:"%d seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",w:"a week",ww:"%d weeks",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},months:Te,monthsShort:Ne,week:{dow:0,doy:6},weekdays:Ze,weekdaysMin:$e,weekdaysShort:ze,meridiemParse:/[ap]\.?m?\.?/i},it={},rt={};function at(e){return e?e.toLowerCase().replace("_","-"):e}function ot(e){for(var t,n,s,i,r=0;r=t&&function(e,t){for(var n=Math.min(e.length,t.length),s=0;s=t-1)break;t--}r++}return nt}function ut(t){var e;if(void 0===it[t]&&"undefined"!=typeof module&&module&&module.exports)try{e=nt._abbr,require("./locale/"+t),lt(e)}catch(e){it[t]=null}return it[t]}function lt(e,t){var n;return e&&((n=r(t)?dt(e):ht(e,t))?nt=n:"undefined"!=typeof console&&console.warn&&console.warn("Locale "+e+" not found. Did you forget to load it?")),nt._abbr}function ht(e,t){if(null===t)return delete it[e],null;var n,s=st;if(t.abbr=e,null!=it[e])Y("defineLocaleOverride","use moment.updateLocale(localeName, config) to change an existing locale. moment.defineLocale(localeName, config) should only be used for creating a new locale See http://momentjs.com/guides/#/warnings/define-locale/ for more info."),s=it[e]._config;else if(null!=t.parentLocale)if(null!=it[t.parentLocale])s=it[t.parentLocale]._config;else{if(null==(n=ut(t.parentLocale)))return rt[t.parentLocale]||(rt[t.parentLocale]=[]),rt[t.parentLocale].push({name:e,config:t}),null;s=n._config}return it[e]=new x(b(s,t)),rt[e]&&rt[e].forEach(function(e){ht(e.name,e.config)}),lt(e),it[e]}function dt(e){var t;if(e&&e._locale&&e._locale._abbr&&(e=e._locale._abbr),!e)return nt;if(!o(e)){if(t=ut(e))return t;e=[e]}return ot(e)}function ct(e){var t,n=e._a;return n&&-2===y(e).overflow&&(t=n[ve]<0||11xe(n[pe],n[ve])?ke:n[Me]<0||24je(n,r,a)?y(e)._overflowWeeks=!0:null!=u?y(e)._overflowWeekday=!0:(o=Ee(n,s,i,r,a),e._a[pe]=o.year,e._dayOfYear=o.dayOfYear)}(e),null!=e._dayOfYear&&(r=St(e._a[pe],s[pe]),(e._dayOfYear>Fe(r)||0===e._dayOfYear)&&(y(e)._overflowDayOfYear=!0),n=Ve(r,0,e._dayOfYear),e._a[ve]=n.getUTCMonth(),e._a[ke]=n.getUTCDate()),t=0;t<3&&null==e._a[t];++t)e._a[t]=u[t]=s[t];for(;t<7;t++)e._a[t]=u[t]=null==e._a[t]?2===t?1:0:e._a[t];24===e._a[Me]&&0===e._a[De]&&0===e._a[Se]&&0===e._a[Ye]&&(e._nextDay=!0,e._a[Me]=0),e._d=(e._useUTC?Ve:function(e,t,n,s,i,r,a){var o;return e<100&&0<=e?(o=new Date(e+400,t,n,s,i,r,a),isFinite(o.getFullYear())&&o.setFullYear(e)):o=new Date(e,t,n,s,i,r,a),o}).apply(null,u),i=e._useUTC?e._d.getUTCDay():e._d.getDay(),null!=e._tzm&&e._d.setUTCMinutes(e._d.getUTCMinutes()-e._tzm),e._nextDay&&(e._a[Me]=24),e._w&&void 0!==e._w.d&&e._w.d!==i&&(y(e).weekdayMismatch=!0)}}function Ot(e){if(e._f!==f.ISO_8601)if(e._f!==f.RFC_2822){e._a=[],y(e).empty=!0;for(var t,n,s,i,r,a,o,u=""+e._i,l=u.length,h=0,d=H(e._f,e._locale).match(N)||[],c=0;cn.valueOf():n.valueOf()"}),pn.toJSON=function(){return this.isValid()?this.toISOString():null},pn.toString=function(){return this.clone().locale("en").format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")},pn.unix=function(){return Math.floor(this.valueOf()/1e3)},pn.valueOf=function(){return this._d.valueOf()-6e4*(this._offset||0)},pn.creationData=function(){return{input:this._i,format:this._f,locale:this._locale,isUTC:this._isUTC,strict:this._strict}},pn.eraName=function(){for(var e,t=this.localeData().eras(),n=0,s=t.length;nthis.clone().month(0).utcOffset()||this.utcOffset()>this.clone().month(5).utcOffset()},pn.isLocal=function(){return!!this.isValid()&&!this._isUTC},pn.isUtcOffset=function(){return!!this.isValid()&&this._isUTC},pn.isUtc=At,pn.isUTC=At,pn.zoneAbbr=function(){return this._isUTC?"UTC":""},pn.zoneName=function(){return this._isUTC?"Coordinated Universal Time":""},pn.dates=n("dates accessor is deprecated. Use date instead.",fn),pn.months=n("months accessor is deprecated. Use month instead",Ue),pn.years=n("years accessor is deprecated. Use year instead",Le),pn.zone=n("moment().zone is deprecated, use moment().utcOffset instead. http://momentjs.com/guides/#/warnings/zone/",function(e,t){return null!=e?("string"!=typeof e&&(e=-e),this.utcOffset(e,t),this):-this.utcOffset()}),pn.isDSTShifted=n("isDSTShifted is deprecated. See http://momentjs.com/guides/#/warnings/dst-shifted/ for more information",function(){if(!r(this._isDSTShifted))return this._isDSTShifted;var e,t={};return v(t,this),(t=bt(t))._a?(e=(t._isUTC?_:Tt)(t._a),this._isDSTShifted=this.isValid()&&0= 2.6.0. You are using Moment.js "+s.version+". See momentjs.com"),d.prototype={_set:function(t){this.name=t.name,this.abbrs=t.abbrs,this.untils=t.untils,this.offsets=t.offsets,this.population=t.population},_index:function(t){for(var e=+t,n=this.untils,o=0;o= 2.9.0. You are using Moment.js "+s.version+"."),s.defaultZone=t?S(t):null,s};var F=s.momentProperties;return"[object Array]"===Object.prototype.toString.call(F)?(F.push("_z"),F.push("_a")):F&&(F._z=null),s});}
+
+window.React = Horizon;
+window.ReactDOM = Horizon;
+window.ReactRedux = HorizonRedux;