!133 test(novdom): switch to vitest

* test(novdom): switch to vitest
* test(novdom): setup vitest
This commit is contained in:
Hoikan 2024-02-05 06:59:50 +00:00 committed by 陈超涛
parent 332bd7ae47
commit 6ad2082444
6 changed files with 212 additions and 106 deletions

View File

@ -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"
} }
} }

View File

@ -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',
};

View File

@ -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('');
});
});
});

View File

@ -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>'

View File

@ -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();
},
});

View File

@ -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'
},
}); });