!133 test(novdom): switch to vitest
* test(novdom): switch to vitest * test(novdom): setup vitest
This commit is contained in:
parent
332bd7ae47
commit
6ad2082444
|
@ -4,9 +4,12 @@
|
||||||
"description": "no vdom runtime",
|
"description": "no vdom runtime",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "jest --config=jest.config.js"
|
"test": "vitest --ui"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"inula-reactive": "workspace:^0.0.1"
|
"inula-reactive": "workspace:^0.0.1",
|
||||||
|
"jsdom": "^24.0.0",
|
||||||
|
"@vitest/ui": "^0.34.5",
|
||||||
|
"vitest": "^0.34.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2023 Huawei Technologies Co.,Ltd.
|
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
||||||
*
|
*
|
||||||
* openInula is licensed under Mulan PSL v2.
|
* openInula is licensed under Mulan PSL v2.
|
||||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||||
|
@ -13,21 +13,6 @@
|
||||||
* See the Mulan PSL v2 for more details.
|
* See the Mulan PSL v2 for more details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
module.exports = {
|
// TODO: JSX type
|
||||||
coverageDirectory: 'coverage',
|
export type FunctionComponent<Props = {}> = (props: Props) => unknown;
|
||||||
resetModules: true,
|
export type AppDisposer = () => void;
|
||||||
|
|
||||||
rootDir: process.cwd(),
|
|
||||||
|
|
||||||
setupFilesAfterEnv: [require.resolve('./tests/jest/jestSetting.js')],
|
|
||||||
|
|
||||||
testEnvironment: 'jest-environment-jsdom-sixteen',
|
|
||||||
|
|
||||||
testMatch: [
|
|
||||||
'<rootDir>/tests/**/*.test.js',
|
|
||||||
'<rootDir>/tests/**/*.test.ts',
|
|
||||||
'<rootDir>/tests/**/*.test.tsx',
|
|
||||||
],
|
|
||||||
|
|
||||||
timers: 'fake',
|
|
||||||
};
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
import { template, insert, setAttribute, className } from '../src/dom';
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
|
||||||
|
describe('DOM manipulation functions', () => {
|
||||||
|
describe('template function', () => {
|
||||||
|
it('should create a node from HTML string', () => {
|
||||||
|
const node = template('<div>Test</div>')();
|
||||||
|
expect(node.outerHTML).toBe('<div>Test</div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a clone of the node on subsequent calls', () => {
|
||||||
|
const createNode = template('<div>Test</div>');
|
||||||
|
const node1 = createNode();
|
||||||
|
const node2 = createNode();
|
||||||
|
expect(node1.isEqualNode(node2)).toBe(true);
|
||||||
|
expect(node1).not.toBe(node2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('insert function', () => {
|
||||||
|
it('should insert a text node into a parent node', () => {
|
||||||
|
const parent = document.createElement('div');
|
||||||
|
insert(parent, 'Test');
|
||||||
|
expect(parent.textContent).toBe('Test');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should replace existing content in the parent node', () => {
|
||||||
|
const parent = document.createElement('div');
|
||||||
|
parent.textContent = 'Old content';
|
||||||
|
insert(parent, 'New content');
|
||||||
|
expect(parent.textContent).toBe('New content');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setAttribute function', () => {
|
||||||
|
it('should set an attribute on a node', () => {
|
||||||
|
const node = document.createElement('div');
|
||||||
|
setAttribute(node, 'test', 'value');
|
||||||
|
expect(node.getAttribute('test')).toBe('value');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove an attribute from a node if value is null', () => {
|
||||||
|
const node = document.createElement('div');
|
||||||
|
node.setAttribute('test', 'value');
|
||||||
|
setAttribute(node, 'test', null);
|
||||||
|
expect(node.hasAttribute('test')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('className function', () => {
|
||||||
|
it('should set the class of a node', () => {
|
||||||
|
const node = document.createElement('div');
|
||||||
|
className(node, 'test');
|
||||||
|
expect(node.className).toBe('test');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove the class from a node if value is null', () => {
|
||||||
|
const node = document.createElement('div');
|
||||||
|
node.className = 'test';
|
||||||
|
className(node, null);
|
||||||
|
expect(node.className).toBe('');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -17,65 +17,49 @@ import { computed, reactive, watch } from 'inula-reactive';
|
||||||
import { template as _$template, insert as _$insert, setAttribute as _$setAttribute } from '../src/dom';
|
import { template as _$template, insert as _$insert, setAttribute as _$setAttribute } from '../src/dom';
|
||||||
import { createComponent as _$createComponent, render } from '../src/core';
|
import { createComponent as _$createComponent, render } from '../src/core';
|
||||||
import { delegateEvents as _$delegateEvents, addEventListener as _$addEventListener } from '../src/event';
|
import { delegateEvents as _$delegateEvents, addEventListener as _$addEventListener } from '../src/event';
|
||||||
|
import { describe, expect } from 'vitest';
|
||||||
|
import { domTest as it } from './utils';
|
||||||
import { Show } from '../src/components/Show';
|
import { Show } from '../src/components/Show';
|
||||||
import { For } from '../src/components/For';
|
import { For } from '../src/components/For';
|
||||||
|
|
||||||
describe('test no-vdom', () => {
|
describe('insertion', () => {
|
||||||
it('简单的使用signal', () => {
|
it('should support placeholder', ({ container }) => {
|
||||||
/**
|
/**
|
||||||
* 源码:
|
* 源码:
|
||||||
|
* const count = reactive(0);
|
||||||
* const CountingComponent = () => {
|
* const CountingComponent = () => {
|
||||||
* const [count, setCount] = useSignal(0);
|
|
||||||
*
|
|
||||||
* return <div id="count">Count value is {count()}.</div>;
|
* return <div id="count">Count value is {count()}.</div>;
|
||||||
* };
|
* };
|
||||||
*
|
*
|
||||||
* render(() => <CountingComponent />, container);
|
* render(() => <CountingComponent />, container);
|
||||||
*/
|
*/
|
||||||
|
const count = reactive(0);
|
||||||
let g_count;
|
|
||||||
|
|
||||||
// 编译后:
|
// 编译后:
|
||||||
const _tmpl$ = /*#__PURE__*/ _$template(`<div id="count">Count value is <!>.`);
|
const _tmpl$ = /*#__PURE__*/ _$template(`<div id="count">Count value is <!>.`);
|
||||||
const CountingComponent = () => {
|
const CountingComponent = () => {
|
||||||
const count = reactive(0);
|
return (() => {
|
||||||
g_count = count;
|
const _el$ = _tmpl$(),
|
||||||
let View
|
|
||||||
|
|
||||||
watch: if (count > 0) {
|
|
||||||
View.$viewValue = createView(() => {})
|
|
||||||
}
|
|
||||||
|
|
||||||
View = createView((() => {
|
|
||||||
const _el$ = tmp.cloneNode(true),
|
|
||||||
_el$2 = _el$.firstChild,
|
_el$2 = _el$.firstChild,
|
||||||
_el$4 = _el$2.nextSibling,
|
_el$4 = _el$2.nextSibling,
|
||||||
_el$3 = _el$4.nextSibling;
|
_el$3 = _el$4.nextSibling;
|
||||||
_$insert(_el$, count, _el$4);
|
_$insert(_el$, count, _el$4);
|
||||||
return _el$;
|
return _el$;
|
||||||
})());
|
})();
|
||||||
|
|
||||||
return View
|
|
||||||
};
|
};
|
||||||
function createView(el) {
|
|
||||||
Object.defineProperty(el, '$viewValue', {
|
|
||||||
set: value => {
|
|
||||||
el.replaceWith(value);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
render(() => _$createComponent(CountingComponent, {}), container);
|
render(() => _$createComponent(CountingComponent, {}), container);
|
||||||
|
|
||||||
expect(container.querySelector('#count').innerHTML).toEqual('Count value is 0<!---->.');
|
expect(container.querySelector('#count').innerHTML).toEqual('Count value is 0<!---->.');
|
||||||
|
|
||||||
g_count.set(c => c + 1);
|
count.set(c => c + 1);
|
||||||
|
|
||||||
expect(container.querySelector('#count').innerHTML).toEqual('Count value is 1<!---->.');
|
expect(container.querySelector('#count').innerHTML).toEqual('Count value is 1<!---->.');
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('return数组,click事件', () => {
|
describe('test no-vdom', () => {
|
||||||
|
it('return数组,click事件', ({ container }) => {
|
||||||
/**
|
/**
|
||||||
* 源码:
|
* 源码:
|
||||||
* const CountingComponent = () => {
|
* const CountingComponent = () => {
|
||||||
|
@ -124,7 +108,7 @@ describe('test no-vdom', () => {
|
||||||
expect(container.querySelector('#count').innerHTML).toEqual('Count value is 1<!---->.');
|
expect(container.querySelector('#count').innerHTML).toEqual('Count value is 1<!---->.');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('return 自定义组件', () => {
|
it('return 自定义组件', ({ container }) => {
|
||||||
/**
|
/**
|
||||||
* 源码:
|
* 源码:
|
||||||
* const CountValue = (props) => {
|
* const CountValue = (props) => {
|
||||||
|
@ -187,7 +171,7 @@ describe('test no-vdom', () => {
|
||||||
expect(container.querySelector('#count').innerHTML).toEqual('Count value is 1<!---->.');
|
expect(container.querySelector('#count').innerHTML).toEqual('Count value is 1<!---->.');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('使用Show组件', () => {
|
it('使用Show组件', ({ container }) => {
|
||||||
/**
|
/**
|
||||||
* 源码:
|
* 源码:
|
||||||
* const CountValue = (props) => {
|
* const CountValue = (props) => {
|
||||||
|
@ -266,7 +250,7 @@ describe('test no-vdom', () => {
|
||||||
expect(container.querySelector('#count').innerHTML).toEqual('Count value is 1<!---->.');
|
expect(container.querySelector('#count').innerHTML).toEqual('Count value is 1<!---->.');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('使用For组件', () => {
|
it('使用For组件', ({ container }) => {
|
||||||
/**
|
/**
|
||||||
* 源码:
|
* 源码:
|
||||||
* const Todo = (props) => {
|
* const Todo = (props) => {
|
||||||
|
@ -314,8 +298,10 @@ describe('test no-vdom', () => {
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// 编译后:
|
// 编译后:
|
||||||
const _tmpl$ = /*#__PURE__*/_$template(`<div>Count value is <!>.`),
|
const _tmpl$ = /*#__PURE__*/ _$template(`<div>Count value is <!>.`),
|
||||||
_tmpl$2 = /*#__PURE__*/_$template(`<div><div id="todos"></div><div><button id="btn">add</button></div><div><button id="btn-push">push`);
|
_tmpl$2 = /*#__PURE__*/ _$template(
|
||||||
|
`<div><div id="todos"></div><div><button id="btn">add</button></div><div><button id="btn-push">push`
|
||||||
|
);
|
||||||
const Todo = props => {
|
const Todo = props => {
|
||||||
return (() => {
|
return (() => {
|
||||||
const _el$ = _tmpl$(),
|
const _el$ = _tmpl$(),
|
||||||
|
@ -363,7 +349,7 @@ describe('test no-vdom', () => {
|
||||||
state.todoList.push({
|
state.todoList.push({
|
||||||
id: 27,
|
id: 27,
|
||||||
title: 'Pig',
|
title: 'Pig',
|
||||||
},);
|
});
|
||||||
};
|
};
|
||||||
return (() => {
|
return (() => {
|
||||||
const _el$5 = _tmpl$2(),
|
const _el$5 = _tmpl$2(),
|
||||||
|
@ -413,7 +399,7 @@ describe('test no-vdom', () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('使用effect, setAttribute, addEventListener', () => {
|
it('使用effect, setAttribute, addEventListener', ({ container }) => {
|
||||||
/**
|
/**
|
||||||
* 源码:
|
* 源码:
|
||||||
* const A = ['pretty', 'large', 'big', 'small', 'tall', 'short', 'long', 'handsome', 'plain', 'quaint', 'clean',
|
* const A = ['pretty', 'large', 'big', 'small', 'tall', 'short', 'long', 'handsome', 'plain', 'quaint', 'clean',
|
||||||
|
@ -489,26 +475,56 @@ describe('test no-vdom', () => {
|
||||||
* render(() => <Main />, document.getElementById("app"));
|
* render(() => <Main />, document.getElementById("app"));
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// 编译后:
|
// 编译后:
|
||||||
const _tmpl$ = /*#__PURE__*/_$template(`<tr><td class="col-md-1">`),
|
const _tmpl$ = /*#__PURE__*/ _$template(`<tr><td class="col-md-1">`),
|
||||||
_tmpl$2 = /*#__PURE__*/_$template(`<div class="col-sm-6"><button type="button">`),
|
_tmpl$2 = /*#__PURE__*/ _$template(`<div class="col-sm-6"><button type="button">`),
|
||||||
_tmpl$3 = /*#__PURE__*/_$template(`<div><div><div><div><h1>Horizon-reactive-novnode</h1></div><div><div></div></div></div></div><table><tbody id="tbody">`);
|
_tmpl$3 = /*#__PURE__*/ _$template(
|
||||||
const A = ['pretty', 'large', 'big', 'small', 'tall', 'short', 'long', 'handsome', 'plain', 'quaint', 'clean', 'elegant', 'easy', 'angry', 'crazy', 'helpful', 'mushy', 'odd', 'unsightly', 'adorable', 'important', 'inexpensive', 'cheap', 'expensive', 'fancy'];
|
`<div><div><div><div><h1>Horizon-reactive-novnode</h1></div><div><div></div></div></div></div><table><tbody id="tbody">`
|
||||||
|
);
|
||||||
|
const A = [
|
||||||
|
'pretty',
|
||||||
|
'large',
|
||||||
|
'big',
|
||||||
|
'small',
|
||||||
|
'tall',
|
||||||
|
'short',
|
||||||
|
'long',
|
||||||
|
'handsome',
|
||||||
|
'plain',
|
||||||
|
'quaint',
|
||||||
|
'clean',
|
||||||
|
'elegant',
|
||||||
|
'easy',
|
||||||
|
'angry',
|
||||||
|
'crazy',
|
||||||
|
'helpful',
|
||||||
|
'mushy',
|
||||||
|
'odd',
|
||||||
|
'unsightly',
|
||||||
|
'adorable',
|
||||||
|
'important',
|
||||||
|
'inexpensive',
|
||||||
|
'cheap',
|
||||||
|
'expensive',
|
||||||
|
'fancy',
|
||||||
|
];
|
||||||
const random = max => Math.round(Math.random() * 1000) % max;
|
const random = max => Math.round(Math.random() * 1000) % max;
|
||||||
let nextId = 1;
|
let nextId = 1;
|
||||||
|
|
||||||
function buildData(count) {
|
function buildData(count) {
|
||||||
let data = new Array(count);
|
let data = new Array(count);
|
||||||
for (let i = 0; i < count; i++) {
|
for (let i = 0; i < count; i++) {
|
||||||
data[i] = {
|
data[i] = {
|
||||||
id: nextId++,
|
id: nextId++,
|
||||||
label: `${A[random(A.length)]}`
|
label: `${A[random(A.length)]}`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Row = props => {
|
const Row = props => {
|
||||||
const selected = computed(() => {
|
const selected = computed(() => {
|
||||||
return props.item.selected.get() ? "danger" : "";
|
return props.item.selected.get() ? 'danger' : '';
|
||||||
});
|
});
|
||||||
|
|
||||||
return (() => {
|
return (() => {
|
||||||
|
@ -523,33 +539,40 @@ describe('test no-vdom', () => {
|
||||||
get each() {
|
get each() {
|
||||||
return props.list;
|
return props.list;
|
||||||
},
|
},
|
||||||
children: item => _$createComponent(Row, {
|
children: item =>
|
||||||
item: item
|
_$createComponent(Row, {
|
||||||
})
|
item: item,
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
const Button = props => (() => {
|
const Button = props =>
|
||||||
const _el$3 = _tmpl$2(),
|
(() => {
|
||||||
_el$4 = _el$3.firstChild;
|
const _el$3 = _tmpl$2(),
|
||||||
_$addEventListener(_el$4, "click", props.cb, true);
|
_el$4 = _el$3.firstChild;
|
||||||
_$insert(_el$4, () => props.title);
|
_$addEventListener(_el$4, 'click', props.cb, true);
|
||||||
watch(() => _$setAttribute(_el$4, "id", props.id));
|
_$insert(_el$4, () => props.title);
|
||||||
return _el$3;
|
watch(() => _$setAttribute(_el$4, 'id', props.id));
|
||||||
})();
|
return _el$3;
|
||||||
|
})();
|
||||||
const Main = () => {
|
const Main = () => {
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
list: [{
|
list: [
|
||||||
id: 1,
|
{
|
||||||
label: '111'
|
id: 1,
|
||||||
}, {
|
label: '111',
|
||||||
id: 2,
|
},
|
||||||
label: '222'
|
{
|
||||||
}],
|
id: 2,
|
||||||
num: 2
|
label: '222',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
num: 2,
|
||||||
});
|
});
|
||||||
|
|
||||||
function run() {
|
function run() {
|
||||||
state.list.set(buildData(5));
|
state.list.set(buildData(5));
|
||||||
}
|
}
|
||||||
|
|
||||||
return (() => {
|
return (() => {
|
||||||
const _el$5 = _tmpl$3(),
|
const _el$5 = _tmpl$3(),
|
||||||
_el$6 = _el$5.firstChild,
|
_el$6 = _el$5.firstChild,
|
||||||
|
@ -559,21 +582,27 @@ describe('test no-vdom', () => {
|
||||||
_el$10 = _el$9.firstChild,
|
_el$10 = _el$9.firstChild,
|
||||||
_el$11 = _el$6.nextSibling,
|
_el$11 = _el$6.nextSibling,
|
||||||
_el$12 = _el$11.firstChild;
|
_el$12 = _el$11.firstChild;
|
||||||
_$insert(_el$10, _$createComponent(Button, {
|
_$insert(
|
||||||
id: "run",
|
_el$10,
|
||||||
title: "Create 1,000 rows",
|
_$createComponent(Button, {
|
||||||
cb: run
|
id: 'run',
|
||||||
}));
|
title: 'Create 1,000 rows',
|
||||||
_$insert(_el$12, _$createComponent(RowList, {
|
cb: run,
|
||||||
get list() {
|
})
|
||||||
return state.list;
|
);
|
||||||
}
|
_$insert(
|
||||||
}));
|
_el$12,
|
||||||
|
_$createComponent(RowList, {
|
||||||
|
get list() {
|
||||||
|
return state.list;
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
return _el$5;
|
return _el$5;
|
||||||
})();
|
})();
|
||||||
};
|
};
|
||||||
render(() => _$createComponent(Main, {}), container);
|
render(() => _$createComponent(Main, {}), container);
|
||||||
_$delegateEvents(["click"]);
|
_$delegateEvents(['click']);
|
||||||
|
|
||||||
expect(container.querySelector('#tbody').innerHTML).toEqual(
|
expect(container.querySelector('#tbody').innerHTML).toEqual(
|
||||||
'<tr><td class="col-md-1">111</td></tr><tr><td class="col-md-1">222</td></tr>'
|
'<tr><td class="col-md-1">111</td></tr><tr><td class="col-md-1">222</td></tr>'
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
||||||
|
*
|
||||||
|
* openInula is licensed under Mulan PSL v2.
|
||||||
|
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||||
|
* You may obtain a copy of Mulan PSL v2 at:
|
||||||
|
*
|
||||||
|
* http://license.coscl.org.cn/MulanPSL2
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||||
|
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||||
|
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||||
|
* See the Mulan PSL v2 for more details.
|
||||||
|
*/
|
||||||
|
import { test } from 'vitest';
|
||||||
|
|
||||||
|
interface DomTestContext {
|
||||||
|
container: HTMLDivElement;
|
||||||
|
}
|
||||||
|
// Define a new test type that extends the default test type and adds the container fixture.
|
||||||
|
export const domTest = test.extend<DomTestContext>({
|
||||||
|
container: async ({ task }, use) => {
|
||||||
|
const container = document.createElement('div');
|
||||||
|
document.body.appendChild(container);
|
||||||
|
await use(container);
|
||||||
|
container.remove();
|
||||||
|
},
|
||||||
|
});
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2023 Huawei Technologies Co.,Ltd.
|
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
||||||
*
|
*
|
||||||
* openInula is licensed under Mulan PSL v2.
|
* openInula is licensed under Mulan PSL v2.
|
||||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||||
|
@ -13,14 +13,11 @@
|
||||||
* See the Mulan PSL v2 for more details.
|
* See the Mulan PSL v2 for more details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
global.container = null;
|
// vitest.config.ts
|
||||||
global.beforeEach(() => {
|
import { defineConfig } from 'vitest/config';
|
||||||
// 创建一个 DOM 元素作为渲染目标
|
|
||||||
global.container = document.createElement('div');
|
|
||||||
document.body.appendChild(global.container);
|
|
||||||
});
|
|
||||||
|
|
||||||
global.afterEach(() => {
|
export default defineConfig({
|
||||||
global.container.remove();
|
test: {
|
||||||
global.container = null;
|
environment: 'jsdom', // or 'jsdom', 'node'
|
||||||
|
},
|
||||||
});
|
});
|
Loading…
Reference in New Issue