@govela/testing 0.0.0-stage → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +45 -2
- package/index.d.ts +72 -0
- package/index.js +117 -0
- package/package.json +26 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 abullo
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,46 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @govela/testing
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Test helpers for Vela components, for Vitest with happy-dom or jsdom.
|
|
4
|
+
The components are compiled by `@govela/unplugin`, as in the
|
|
5
|
+
application.
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm i -D @govela/testing @govela/unplugin vitest happy-dom
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
// vite.config.js
|
|
13
|
+
import vela from '@govela/unplugin/vite';
|
|
14
|
+
|
|
15
|
+
export default { plugins: [vela()], test: { environment: 'happy-dom' } };
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
```js
|
|
19
|
+
import { afterEach, expect, test } from 'vitest';
|
|
20
|
+
import { cleanup, fireEvent, render } from '@govela/testing';
|
|
21
|
+
import Counter from './Counter.vela';
|
|
22
|
+
|
|
23
|
+
afterEach(cleanup);
|
|
24
|
+
|
|
25
|
+
test('counts', async () => {
|
|
26
|
+
const { container } = render(Counter, { props: { step: 2 } });
|
|
27
|
+
await fireEvent.click(container.querySelector('button'));
|
|
28
|
+
expect(container.textContent).toContain('2');
|
|
29
|
+
});
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
- `render(Component, { props, slots, events, target })` mounts the
|
|
33
|
+
component into a new `<div>` of `document.body` and returns
|
|
34
|
+
`{ container, component, rerender(props), unmount() }`. A slot is an
|
|
35
|
+
HTML string, a node or a function returning one; `events` listens to
|
|
36
|
+
the component's `$$dispatch` events.
|
|
37
|
+
- `fireEvent(element, event)`, `fireEvent.click(element)`,
|
|
38
|
+
`fireEvent.input(element, value)` and the others dispatch the event
|
|
39
|
+
and wait until the component has updated.
|
|
40
|
+
- `cleanup()` unmounts every rendered component; `tick()` waits for
|
|
41
|
+
pending updates.
|
|
42
|
+
- In TypeScript, `render` checks the props (and requires the required
|
|
43
|
+
ones), the `detail` of each listener and the slot names against the
|
|
44
|
+
component's type.
|
|
45
|
+
|
|
46
|
+
License: MIT.
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// Types of @govela/testing. render() checks the props, events and slots of
|
|
2
|
+
// a test against the component's types (Component<P, E, S, B>).
|
|
3
|
+
import type { Component, ComponentInstance, Props } from '@govela/runtime';
|
|
4
|
+
|
|
5
|
+
export { tick } from '@govela/runtime';
|
|
6
|
+
|
|
7
|
+
/** Content of a slot: an HTML string, a node, or a function returning one. */
|
|
8
|
+
export type SlotContent = string | Node | (() => string | Node);
|
|
9
|
+
|
|
10
|
+
/** Listeners of a component's events, by name, with their `detail` types. */
|
|
11
|
+
export type EventListeners<E extends Record<string, any>> = {
|
|
12
|
+
[K in keyof E & string]?: (event: CustomEvent<E[K]>) => void;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/** The slots of a component by name; `default` is the default slot. */
|
|
16
|
+
export type SlotContents<S extends Record<string, any>> = string extends keyof S
|
|
17
|
+
? Record<string, SlotContent>
|
|
18
|
+
: { [K in keyof S]?: SlotContent };
|
|
19
|
+
|
|
20
|
+
/** The options of render(): props are required when the component has required props. */
|
|
21
|
+
export type RenderOptions<
|
|
22
|
+
P extends Props = Props,
|
|
23
|
+
E extends Record<string, any> = Record<string, any>,
|
|
24
|
+
S extends Record<string, any> = Record<string, any>,
|
|
25
|
+
> = {
|
|
26
|
+
/** Slot contents by name. */
|
|
27
|
+
slots?: SlotContents<S>;
|
|
28
|
+
/** Listeners of the component's events (`$$dispatch`), by name. */
|
|
29
|
+
events?: EventListeners<E>;
|
|
30
|
+
/** The element to mount into; by default a new div in document.body. */
|
|
31
|
+
target?: Element;
|
|
32
|
+
} & ({} extends P ? { props?: P } : { props: P });
|
|
33
|
+
|
|
34
|
+
export interface RenderResult<P extends Props = Props> {
|
|
35
|
+
/** The element the component is mounted into. */
|
|
36
|
+
readonly container: Element;
|
|
37
|
+
/** The mounted component. */
|
|
38
|
+
readonly component: ComponentInstance<P>;
|
|
39
|
+
/** Sets props and waits for the update. */
|
|
40
|
+
rerender(props: Partial<P>): Promise<void>;
|
|
41
|
+
/** Destroys the component and removes the container render() created. */
|
|
42
|
+
unmount(): void;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Mounts a component for a test. */
|
|
46
|
+
export function render<P extends Props, E extends Record<string, any>, S extends Record<string, any>>(
|
|
47
|
+
component: Component<P, E, S, any>,
|
|
48
|
+
...options: {} extends P ? [options?: RenderOptions<NoInfer<P>, NoInfer<E>, NoInfer<S>>] : [options: RenderOptions<NoInfer<P>, NoInfer<E>, NoInfer<S>>]
|
|
49
|
+
): RenderResult<P>;
|
|
50
|
+
|
|
51
|
+
/** Unmounts every component render() mounted; call it after each test. */
|
|
52
|
+
export function cleanup(): void;
|
|
53
|
+
|
|
54
|
+
export interface FireEvent {
|
|
55
|
+
/** Dispatches event on element and waits for the component to update. */
|
|
56
|
+
(element: Element | null, event: Event): Promise<boolean>;
|
|
57
|
+
click(element: Element | null, init?: MouseEventInit): Promise<boolean>;
|
|
58
|
+
dblClick(element: Element | null, init?: MouseEventInit): Promise<boolean>;
|
|
59
|
+
mouseDown(element: Element | null, init?: MouseEventInit): Promise<boolean>;
|
|
60
|
+
mouseUp(element: Element | null, init?: MouseEventInit): Promise<boolean>;
|
|
61
|
+
keyDown(element: Element | null, init?: KeyboardEventInit): Promise<boolean>;
|
|
62
|
+
keyUp(element: Element | null, init?: KeyboardEventInit): Promise<boolean>;
|
|
63
|
+
focus(element: Element | null, init?: FocusEventInit): Promise<boolean>;
|
|
64
|
+
blur(element: Element | null, init?: FocusEventInit): Promise<boolean>;
|
|
65
|
+
/** Sets the value of an input, select or textarea and dispatches input. */
|
|
66
|
+
input(element: Element | null, value?: string, init?: EventInit): Promise<boolean>;
|
|
67
|
+
/** Sets the value (or checked, for a boolean) and dispatches change. */
|
|
68
|
+
change(element: Element | null, value?: string | boolean, init?: EventInit): Promise<boolean>;
|
|
69
|
+
submit(element: Element | null, init?: EventInit): Promise<boolean>;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export const fireEvent: FireEvent;
|
package/index.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// @govela/testing: render Vela components in a test DOM (Vitest with
|
|
2
|
+
// happy-dom or jsdom) and act on them (task 066, docs/GUIDE.md "Testing").
|
|
3
|
+
// Compile the components with @govela/unplugin, as in the application.
|
|
4
|
+
import { mount, tick } from '@govela/runtime';
|
|
5
|
+
|
|
6
|
+
export { tick };
|
|
7
|
+
|
|
8
|
+
const rendered = new Set();
|
|
9
|
+
|
|
10
|
+
// A slot passed to render(): an HTML string, a node, or a function returning
|
|
11
|
+
// either. A slot of the runtime is a function returning a DocumentFragment
|
|
12
|
+
// with the content; reopened, it returns the same nodes.
|
|
13
|
+
function slotOf(content) {
|
|
14
|
+
let nodes = null;
|
|
15
|
+
return () => {
|
|
16
|
+
if (!nodes) {
|
|
17
|
+
const value = typeof content === 'function' ? content() : content;
|
|
18
|
+
const template = document.createElement('template');
|
|
19
|
+
if (typeof value === 'string') template.innerHTML = value;
|
|
20
|
+
else if (value != null) template.content.append(value);
|
|
21
|
+
nodes = [...template.content.childNodes];
|
|
22
|
+
}
|
|
23
|
+
const fragment = document.createDocumentFragment();
|
|
24
|
+
fragment.append(...nodes);
|
|
25
|
+
return fragment;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Mounts a component into a new container appended to document.body (or
|
|
31
|
+
* into options.target) and returns the container and the instance.
|
|
32
|
+
*/
|
|
33
|
+
export function render(Component, options = {}) {
|
|
34
|
+
const { props = {}, slots = {}, events = {}, target } = options;
|
|
35
|
+
const container = target ?? document.body.appendChild(document.createElement('div'));
|
|
36
|
+
const runtimeSlots = {};
|
|
37
|
+
for (const [name, content] of Object.entries(slots)) runtimeSlots[name] = slotOf(content);
|
|
38
|
+
// Component events are CustomEvents that bubble from the component's DOM.
|
|
39
|
+
const listeners = [];
|
|
40
|
+
for (const [name, handler] of Object.entries(events)) {
|
|
41
|
+
container.addEventListener(name, handler);
|
|
42
|
+
listeners.push([name, handler]);
|
|
43
|
+
}
|
|
44
|
+
const component = mount(Component, { target: container, props, slots: runtimeSlots });
|
|
45
|
+
let mounted = true;
|
|
46
|
+
const result = {
|
|
47
|
+
container,
|
|
48
|
+
component,
|
|
49
|
+
/** Sets props and waits for the update. */
|
|
50
|
+
async rerender(next) {
|
|
51
|
+
if (!component.set) throw new Error('[Vela] rerender(): the component has no props');
|
|
52
|
+
component.set(next);
|
|
53
|
+
await settle();
|
|
54
|
+
},
|
|
55
|
+
/** Destroys the component and removes the container it created. */
|
|
56
|
+
unmount() {
|
|
57
|
+
if (!mounted) return;
|
|
58
|
+
mounted = false;
|
|
59
|
+
rendered.delete(result);
|
|
60
|
+
component.destroy();
|
|
61
|
+
for (const [name, handler] of listeners) container.removeEventListener(name, handler);
|
|
62
|
+
if (!target) container.remove();
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
rendered.add(result);
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Unmounts every component render() mounted; call it after each test. */
|
|
70
|
+
export function cleanup() {
|
|
71
|
+
for (const result of [...rendered]) result.unmount();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// settle waits until the updates scheduled by an event or a state change
|
|
75
|
+
// are applied: the runtime flushes them in microtasks.
|
|
76
|
+
async function settle() {
|
|
77
|
+
await tick();
|
|
78
|
+
await tick();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Dispatches event on element and waits for the component to update.
|
|
83
|
+
* fireEvent.click(button), fireEvent.input(input, 'text') and the other
|
|
84
|
+
* helpers create the event.
|
|
85
|
+
*/
|
|
86
|
+
export async function fireEvent(element, event) {
|
|
87
|
+
if (!element) throw new Error('[Vela] fireEvent(): no element');
|
|
88
|
+
const result = element.dispatchEvent(event);
|
|
89
|
+
await settle();
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const view = () => document.defaultView ?? globalThis;
|
|
94
|
+
const mouse = (type) => (element, init = {}) => fireEvent(element, new (view().MouseEvent)(type, { bubbles: true, cancelable: true, ...init }));
|
|
95
|
+
const keyboard = (type) => (element, init = {}) => fireEvent(element, new (view().KeyboardEvent)(type, { bubbles: true, cancelable: true, ...init }));
|
|
96
|
+
const focus = (type, bubbles) => (element, init = {}) => fireEvent(element, new (view().FocusEvent)(type, { bubbles, ...init }));
|
|
97
|
+
|
|
98
|
+
fireEvent.click = mouse('click');
|
|
99
|
+
fireEvent.dblClick = mouse('dblclick');
|
|
100
|
+
fireEvent.mouseDown = mouse('mousedown');
|
|
101
|
+
fireEvent.mouseUp = mouse('mouseup');
|
|
102
|
+
fireEvent.keyDown = keyboard('keydown');
|
|
103
|
+
fireEvent.keyUp = keyboard('keyup');
|
|
104
|
+
fireEvent.focus = focus('focus', false);
|
|
105
|
+
fireEvent.blur = focus('blur', false);
|
|
106
|
+
/** Sets the value of an input, select or textarea and dispatches input. */
|
|
107
|
+
fireEvent.input = (element, value, init = {}) => {
|
|
108
|
+
if (value !== undefined) element.value = value;
|
|
109
|
+
return fireEvent(element, new (view().Event)('input', { bubbles: true, ...init }));
|
|
110
|
+
};
|
|
111
|
+
/** Sets the value (or checked, for a boolean) and dispatches change. */
|
|
112
|
+
fireEvent.change = (element, value, init = {}) => {
|
|
113
|
+
if (typeof value === 'boolean') element.checked = value;
|
|
114
|
+
else if (value !== undefined) element.value = value;
|
|
115
|
+
return fireEvent(element, new (view().Event)('change', { bubbles: true, ...init }));
|
|
116
|
+
};
|
|
117
|
+
fireEvent.submit = (element, init = {}) => fireEvent(element, new (view().Event)('submit', { bubbles: true, cancelable: true, ...init }));
|
package/package.json
CHANGED
|
@@ -1,6 +1,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@govela/testing",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.7.0",
|
|
4
|
+
"description": "Test helpers for Vela components: render, fireEvent, cleanup, tick. Manifest generated by the compiler repository; do not edit.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/elauven/vela.git",
|
|
9
|
+
"directory": "packages/testing"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./index.d.ts",
|
|
15
|
+
"default": "./index.js"
|
|
16
|
+
},
|
|
17
|
+
"./package.json": "./package.json"
|
|
18
|
+
},
|
|
19
|
+
"types": "index.d.ts",
|
|
20
|
+
"files": [
|
|
21
|
+
"index.js",
|
|
22
|
+
"index.d.ts",
|
|
23
|
+
"LICENSE"
|
|
24
|
+
],
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"@govela/runtime": "0.7.0"
|
|
27
|
+
}
|
|
28
|
+
}
|