p-elements-core 1.2.31 → 1.2.32-rc-10

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.
Files changed (82) hide show
  1. package/.editorconfig +17 -17
  2. package/.gitlab-ci.yml +18 -18
  3. package/CHANGELOG.md +201 -0
  4. package/demo/sample.js +1 -1
  5. package/demo/screen.css +16 -16
  6. package/demo/theme.css +1 -0
  7. package/dist/p-elements-core-modern.js +1 -1
  8. package/dist/p-elements-core.js +1 -1
  9. package/docs/package-lock.json +6897 -6897
  10. package/docs/package.json +27 -27
  11. package/docs/src/404.md +8 -8
  12. package/docs/src/_data/demos/hello-world/hello-world.tsx +35 -35
  13. package/docs/src/_data/demos/hello-world/index.html +10 -10
  14. package/docs/src/_data/demos/hello-world/project.json +7 -7
  15. package/docs/src/_data/demos/timer/demo-timer.tsx +120 -120
  16. package/docs/src/_data/demos/timer/icons.tsx +62 -62
  17. package/docs/src/_data/demos/timer/index.html +12 -12
  18. package/docs/src/_data/demos/timer/project.json +8 -8
  19. package/docs/src/_data/global.js +13 -13
  20. package/docs/src/_data/helpers.js +19 -19
  21. package/docs/src/_includes/layouts/base.njk +30 -30
  22. package/docs/src/_includes/layouts/playground.njk +40 -40
  23. package/docs/src/_includes/partials/app-header.njk +8 -8
  24. package/docs/src/_includes/partials/head.njk +14 -14
  25. package/docs/src/_includes/partials/nav.njk +19 -19
  26. package/docs/src/_includes/partials/top-nav.njk +51 -51
  27. package/docs/src/documentation/custom-element.md +221 -221
  28. package/docs/src/documentation/decorators/bind.md +71 -71
  29. package/docs/src/documentation/decorators/custom-element-config.md +63 -63
  30. package/docs/src/documentation/decorators/property.md +83 -83
  31. package/docs/src/documentation/decorators/query.md +66 -66
  32. package/docs/src/documentation/decorators/render-property-on-set.md +60 -60
  33. package/docs/src/documentation/decorators.md +9 -9
  34. package/docs/src/documentation/reactive-properties.md +53 -53
  35. package/docs/src/index.d.ts +25 -25
  36. package/docs/src/index.md +3 -3
  37. package/docs/src/scripts/components/app-mode-switch/app-mode-switch.css +78 -78
  38. package/docs/src/scripts/components/app-mode-switch/app-mode-switch.tsx +166 -166
  39. package/docs/src/scripts/components/app-playground/app-playground.tsx +189 -189
  40. package/docs/tsconfig.json +22 -22
  41. package/index.html +15 -2
  42. package/p-elements-core.d.ts +11 -1
  43. package/package.json +11 -4
  44. package/readme.md +206 -206
  45. package/src/custom-element-controller.test.ts +226 -0
  46. package/src/custom-element-controller.ts +31 -31
  47. package/src/custom-element.test.ts +906 -0
  48. package/src/custom-element.ts +471 -188
  49. package/src/custom-style-element.ts +4 -1
  50. package/src/decorators/bind.test.ts +163 -0
  51. package/src/decorators/bind.ts +46 -46
  52. package/src/decorators/custom-element-config.ts +17 -17
  53. package/src/decorators/property.test.ts +279 -0
  54. package/src/decorators/property.ts +822 -150
  55. package/src/decorators/query.test.ts +146 -0
  56. package/src/decorators/query.ts +12 -12
  57. package/src/decorators/render-property-on-set.ts +3 -3
  58. package/src/helpers/css.test.ts +150 -0
  59. package/src/helpers/css.ts +71 -71
  60. package/src/maquette/cache.test.ts +150 -0
  61. package/src/maquette/cache.ts +35 -35
  62. package/src/maquette/dom.test.ts +263 -0
  63. package/src/maquette/dom.ts +115 -115
  64. package/src/maquette/h.test.ts +165 -0
  65. package/src/maquette/h.ts +100 -100
  66. package/src/maquette/index.ts +12 -12
  67. package/src/maquette/interfaces.ts +536 -536
  68. package/src/maquette/jsx.ts +61 -61
  69. package/src/maquette/mapping.test.ts +294 -0
  70. package/src/maquette/mapping.ts +56 -56
  71. package/src/maquette/maquette.test.ts +493 -0
  72. package/src/maquette/projection.test.ts +366 -0
  73. package/src/maquette/projection.ts +666 -666
  74. package/src/maquette/projector.test.ts +351 -0
  75. package/src/maquette/projector.ts +200 -200
  76. package/src/sample/mixin/highlight.tsx +33 -32
  77. package/src/sample/sample.tsx +167 -7
  78. package/src/test-setup.ts +85 -0
  79. package/src/test-utils.ts +223 -0
  80. package/tsconfig.json +1 -0
  81. package/vitest.config.ts +41 -0
  82. package/webpack.config.js +1 -1
@@ -0,0 +1,263 @@
1
+ /**
2
+ * Tests for Maquette DOM manipulation module
3
+ */
4
+
5
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
6
+ import { dom } from './dom.js';
7
+ import { h } from './h.js';
8
+
9
+ describe('Maquette DOM', () => {
10
+ let container: HTMLDivElement;
11
+
12
+ beforeEach(() => {
13
+ container = document.createElement('div');
14
+ document.body.appendChild(container);
15
+ });
16
+
17
+ afterEach(() => {
18
+ if (container.parentNode) {
19
+ document.body.removeChild(container);
20
+ }
21
+ });
22
+
23
+ describe('dom.create', () => {
24
+ it('should create DOM from VNode', () => {
25
+ const vnode = h('div.test', ['Hello World']);
26
+ const projection = dom.create(vnode);
27
+
28
+ expect(projection).toBeDefined();
29
+ expect(projection.domNode).toBeDefined();
30
+ expect(projection.update).toBeDefined();
31
+ });
32
+
33
+ it('should create nested elements', () => {
34
+ const vnode = h('div', [
35
+ h('span', ['Text 1']),
36
+ h('span', ['Text 2']),
37
+ ]);
38
+ const projection = dom.create(vnode);
39
+
40
+ expect(projection.domNode.children.length).toBe(2);
41
+ expect(projection.domNode.children[0].textContent).toBe('Text 1');
42
+ expect(projection.domNode.children[1].textContent).toBe('Text 2');
43
+ });
44
+
45
+ it('should apply properties', () => {
46
+ const vnode = h('input', {
47
+ type: 'text',
48
+ value: 'test',
49
+ placeholder: 'Enter text'
50
+ });
51
+ const projection = dom.create(vnode);
52
+ const input = projection.domNode as HTMLInputElement;
53
+
54
+ expect(input.type).toBe('text');
55
+ expect(input.value).toBe('test');
56
+ expect(input.placeholder).toBe('Enter text');
57
+ });
58
+
59
+ it('should apply classes', () => {
60
+ const vnode = h('div.class1.class2');
61
+ const projection = dom.create(vnode);
62
+
63
+ expect(projection.domNode.classList.contains('class1')).toBe(true);
64
+ expect(projection.domNode.classList.contains('class2')).toBe(true);
65
+ });
66
+ });
67
+
68
+ describe('dom.append', () => {
69
+ it('should append VNode to parent element', () => {
70
+ const vnode = h('div.child', ['Child content']);
71
+ const projection = dom.append(container, vnode);
72
+
73
+ expect(container.children.length).toBe(1);
74
+ expect(container.children[0].classList.contains('child')).toBe(true);
75
+ expect(container.children[0].textContent).toBe('Child content');
76
+ });
77
+
78
+ it('should append multiple children sequentially', () => {
79
+ dom.append(container, h('div', ['First']));
80
+ dom.append(container, h('div', ['Second']));
81
+ dom.append(container, h('div', ['Third']));
82
+
83
+ expect(container.children.length).toBe(3);
84
+ expect(container.children[0].textContent).toBe('First');
85
+ expect(container.children[1].textContent).toBe('Second');
86
+ expect(container.children[2].textContent).toBe('Third');
87
+ });
88
+ });
89
+
90
+ describe('dom.insertBefore', () => {
91
+ it('should insert VNode before specified node', () => {
92
+ const first = document.createElement('div');
93
+ first.textContent = 'First';
94
+ container.appendChild(first);
95
+
96
+ const last = document.createElement('div');
97
+ last.textContent = 'Last';
98
+ container.appendChild(last);
99
+
100
+ const vnode = h('div', ['Middle']);
101
+ dom.insertBefore(last, vnode);
102
+
103
+ expect(container.children.length).toBe(3);
104
+ expect(container.children[0].textContent).toBe('First');
105
+ expect(container.children[1].textContent).toBe('Middle');
106
+ expect(container.children[2].textContent).toBe('Last');
107
+ });
108
+
109
+ it('should insert at beginning', () => {
110
+ const existing = document.createElement('div');
111
+ existing.textContent = 'Existing';
112
+ container.appendChild(existing);
113
+
114
+ const vnode = h('div', ['New First']);
115
+ dom.insertBefore(existing, vnode);
116
+
117
+ expect(container.children.length).toBe(2);
118
+ expect(container.children[0].textContent).toBe('New First');
119
+ expect(container.children[1].textContent).toBe('Existing');
120
+ });
121
+ });
122
+
123
+ describe('dom.merge', () => {
124
+ it('should merge VNode with existing element', () => {
125
+ const existingDiv = document.createElement('div');
126
+ existingDiv.setAttribute('id', 'existing');
127
+ existingDiv.textContent = 'Original';
128
+ container.appendChild(existingDiv);
129
+
130
+ const vnode = h('div', { 'data-merged': 'true' }, ['Merged']);
131
+ const projection = dom.merge(existingDiv, vnode);
132
+
133
+ expect(projection.domNode).toBe(existingDiv);
134
+ expect(existingDiv.getAttribute('id')).toBe('existing'); // Preserved
135
+ expect(existingDiv.getAttribute('data-merged')).toBe('true'); // Added
136
+ expect(existingDiv.textContent).toBe('Merged'); // Updated
137
+ });
138
+
139
+ it('should preserve existing children when merging', () => {
140
+ const existingDiv = document.createElement('div');
141
+ const existingChild = document.createElement('span');
142
+ existingChild.textContent = 'Existing child';
143
+ existingDiv.appendChild(existingChild);
144
+ container.appendChild(existingDiv);
145
+
146
+ const vnode = h('div', {}, [h('span', ['New child'])]);
147
+ dom.merge(existingDiv, vnode);
148
+
149
+ // Behavior depends on implementation - typically adds new children
150
+ expect(existingDiv.children.length).toBeGreaterThan(0);
151
+ });
152
+ });
153
+
154
+ describe('projection.update', () => {
155
+ it('should update DOM when VNode changes', () => {
156
+ let text = 'Initial';
157
+ const createVNode = () => h('div', [text]);
158
+
159
+ const projection = dom.append(container, createVNode());
160
+ expect(container.children[0].textContent).toBe('Initial');
161
+
162
+ text = 'Updated';
163
+ projection.update(createVNode());
164
+ expect(container.children[0].textContent).toBe('Updated');
165
+ });
166
+
167
+ it('should update properties', () => {
168
+ let value = 'initial';
169
+ const createVNode = () => h('input', { value });
170
+
171
+ const projection = dom.append(container, createVNode());
172
+ const input = container.children[0] as HTMLInputElement;
173
+ expect(input.value).toBe('initial');
174
+
175
+ value = 'updated';
176
+ projection.update(createVNode());
177
+ expect(input.value).toBe('updated');
178
+ });
179
+
180
+ it('should update nested elements', () => {
181
+ let count = 1;
182
+ const createVNode = () => h('div', [
183
+ h('span.count', [`Count: ${count}`]),
184
+ ]);
185
+
186
+ const projection = dom.append(container, createVNode());
187
+ expect(container.querySelector('.count')?.textContent).toBe('Count: 1');
188
+
189
+ count = 5;
190
+ projection.update(createVNode());
191
+ expect(container.querySelector('.count')?.textContent).toBe('Count: 5');
192
+ });
193
+ });
194
+
195
+ describe('projection options', () => {
196
+ it('should apply custom styleApplyer', () => {
197
+ let stylesCalled: Array<{ styleName: string; value: string }> = [];
198
+
199
+ const vnode = h('div', {
200
+ styles: {
201
+ color: 'red',
202
+ fontSize: '16px'
203
+ }
204
+ });
205
+
206
+ const projection = dom.create(vnode, {
207
+ styleApplyer: (domNode, styleName, value) => {
208
+ stylesCalled.push({ styleName, value });
209
+ (domNode.style as any)[styleName] = value;
210
+ },
211
+ });
212
+
213
+ expect(stylesCalled.length).toBeGreaterThan(0);
214
+ });
215
+
216
+ it('should handle CSS variables in styles', () => {
217
+ const vnode = h('div', {
218
+ styles: {
219
+ '--custom-color': 'blue',
220
+ 'color': 'red'
221
+ }
222
+ });
223
+
224
+ const projection = dom.create(vnode);
225
+ const div = projection.domNode as HTMLElement;
226
+
227
+ // CSS variables should be set
228
+ expect(div.style.getPropertyValue('--custom-color')).toBe('blue');
229
+ });
230
+ });
231
+
232
+ describe('edge cases', () => {
233
+ it('should handle empty VNode', () => {
234
+ const vnode = h('div');
235
+ const projection = dom.create(vnode);
236
+
237
+ expect(projection.domNode).toBeDefined();
238
+ expect(projection.domNode.children.length).toBe(0);
239
+ });
240
+
241
+ it('should handle VNode with only text', () => {
242
+ const vnode = h('div', ['Just text']);
243
+ const projection = dom.create(vnode);
244
+
245
+ expect(projection.domNode.textContent).toBe('Just text');
246
+ });
247
+
248
+ it('should handle deeply nested structures', () => {
249
+ const vnode = h('div', [
250
+ h('div', [
251
+ h('div', [
252
+ h('div', [
253
+ h('span', ['Deep'])
254
+ ])
255
+ ])
256
+ ])
257
+ ]);
258
+
259
+ const projection = dom.create(vnode);
260
+ expect(projection.domNode.querySelector('span')?.textContent).toBe('Deep');
261
+ });
262
+ });
263
+ });
@@ -1,115 +1,115 @@
1
- /**
2
- * Contains simple low-level utility functions to manipulate the real DOM.
3
- */
4
- import { Projection, ProjectionOptions, VNode } from "./interfaces";
5
- import { createDom, createProjection, extend, initPropertiesAndChildren } from "./projection";
6
-
7
- const DEFAULT_PROJECTION_OPTIONS: ProjectionOptions = {
8
- namespace: undefined,
9
- performanceLogger: () => undefined,
10
- eventHandlerInterceptor: undefined,
11
- styleApplyer: (domNode: HTMLElement, styleName: string, value: string) => {
12
- if (styleName.charAt(0) === "-") {
13
- // CSS variables must be set using setProperty
14
- domNode.style.setProperty(styleName, value);
15
- } else {
16
- // properties like 'backgroundColor' must be set as a js-property
17
- (domNode.style as any)[styleName] = value;
18
- }
19
- },
20
- };
21
-
22
- export let applyDefaultProjectionOptions = (
23
- projectorOptions?: ProjectionOptions
24
- ): ProjectionOptions => {
25
- return extend(DEFAULT_PROJECTION_OPTIONS, projectorOptions);
26
- };
27
-
28
- export let dom = {
29
- /**
30
- * Creates a real DOM tree from `vnode`. The [[Projection]] object returned will contain the resulting DOM Node in
31
- * its [[Projection.domNode|domNode]] property.
32
- * This is a low-level method. Users will typically use a [[Projector]] instead.
33
- * @param vnode - The root of the virtual DOM tree that was created using the [[h]] function. NOTE: [[VNode]]
34
- * objects may only be rendered once.
35
- * @param projectionOptions - Options to be used to create and update the projection.
36
- * @returns The [[Projection]] which also contains the DOM Node that was created.
37
- */
38
- create: (vnode: VNode, projectionOptions?: ProjectionOptions): Projection => {
39
- projectionOptions = applyDefaultProjectionOptions(projectionOptions);
40
- createDom(vnode, document.createElement("div"), undefined, projectionOptions);
41
- return createProjection(vnode, projectionOptions);
42
- },
43
-
44
- /**
45
- * Appends a new child node to the DOM which is generated from a [[VNode]].
46
- * This is a low-level method. Users will typically use a [[Projector]] instead.
47
- * @param parentNode - The parent node for the new child node.
48
- * @param vnode - The root of the virtual DOM tree that was created using the [[h]] function. NOTE: [[VNode]]
49
- * objects may only be rendered once.
50
- * @param projectionOptions - Options to be used to create and update the [[Projection]].
51
- * @returns The [[Projection]] that was created.
52
- */
53
- append: (
54
- parentNode: Element,
55
- vnode: VNode,
56
- projectionOptions?: ProjectionOptions
57
- ): Projection => {
58
- projectionOptions = applyDefaultProjectionOptions(projectionOptions);
59
- createDom(vnode, parentNode, undefined, projectionOptions);
60
- return createProjection(vnode, projectionOptions);
61
- },
62
-
63
- /**
64
- * Inserts a new DOM node which is generated from a [[VNode]].
65
- * This is a low-level method. Users wil typically use a [[Projector]] instead.
66
- * @param beforeNode - The node that the DOM Node is inserted before.
67
- * @param vnode - The root of the virtual DOM tree that was created using the [[h]] function.
68
- * NOTE: [[VNode]] objects may only be rendered once.
69
- * @param projectionOptions - Options to be used to create and update the projection, see [[createProjector]].
70
- * @returns The [[Projection]] that was created.
71
- */
72
- insertBefore: (
73
- beforeNode: Element,
74
- vnode: VNode,
75
- projectionOptions?: ProjectionOptions
76
- ): Projection => {
77
- projectionOptions = applyDefaultProjectionOptions(projectionOptions);
78
- createDom(vnode, beforeNode.parentNode!, beforeNode, projectionOptions);
79
- return createProjection(vnode, projectionOptions);
80
- },
81
-
82
- /**
83
- * Merges a new DOM node which is generated from a [[VNode]] with an existing DOM Node.
84
- * This means that the virtual DOM and the real DOM will have one overlapping element.
85
- * Therefore the selector for the root [[VNode]] will be ignored, but its properties and children will be applied to the Element provided.
86
- * This is a low-level method. Users wil typically use a [[Projector]] instead.
87
- * @param element - The existing element to adopt as the root of the new virtual DOM. Existing attributes and child nodes are preserved.
88
- * @param vnode - The root of the virtual DOM tree that was created using the [[h]] function. NOTE: [[VNode]] objects
89
- * may only be rendered once.
90
- * @param projectionOptions - Options to be used to create and update the projection, see [[createProjector]].
91
- * @returns The [[Projection]] that was created.
92
- */
93
- merge: (element: Element, vnode: VNode, projectionOptions?: ProjectionOptions): Projection => {
94
- projectionOptions = applyDefaultProjectionOptions(projectionOptions);
95
- vnode.domNode = element;
96
- initPropertiesAndChildren(element, vnode, projectionOptions);
97
- return createProjection(vnode, projectionOptions);
98
- },
99
-
100
- /**
101
- * Replaces an existing DOM node with a node generated from a [[VNode]].
102
- * This is a low-level method. Users will typically use a [[Projector]] instead.
103
- * @param element - The node for the [[VNode]] to replace.
104
- * @param vnode - The root of the virtual DOM tree that was created using the [[h]] function. NOTE: [[VNode]]
105
- * objects may only be rendered once.
106
- * @param projectionOptions - Options to be used to create and update the [[Projection]].
107
- * @returns The [[Projection]] that was created.
108
- */
109
- replace: (element: Element, vnode: VNode, projectionOptions?: ProjectionOptions): Projection => {
110
- projectionOptions = applyDefaultProjectionOptions(projectionOptions);
111
- createDom(vnode, element.parentNode!, element, projectionOptions);
112
- element.parentNode!.removeChild(element);
113
- return createProjection(vnode, projectionOptions);
114
- },
115
- };
1
+ /**
2
+ * Contains simple low-level utility functions to manipulate the real DOM.
3
+ */
4
+ import { Projection, ProjectionOptions, VNode } from "./interfaces";
5
+ import { createDom, createProjection, extend, initPropertiesAndChildren } from "./projection";
6
+
7
+ const DEFAULT_PROJECTION_OPTIONS: ProjectionOptions = {
8
+ namespace: undefined,
9
+ performanceLogger: () => undefined,
10
+ eventHandlerInterceptor: undefined,
11
+ styleApplyer: (domNode: HTMLElement, styleName: string, value: string) => {
12
+ if (styleName.charAt(0) === "-") {
13
+ // CSS variables must be set using setProperty
14
+ domNode.style.setProperty(styleName, value);
15
+ } else {
16
+ // properties like 'backgroundColor' must be set as a js-property
17
+ (domNode.style as any)[styleName] = value;
18
+ }
19
+ },
20
+ };
21
+
22
+ export let applyDefaultProjectionOptions = (
23
+ projectorOptions?: ProjectionOptions
24
+ ): ProjectionOptions => {
25
+ return extend(DEFAULT_PROJECTION_OPTIONS, projectorOptions);
26
+ };
27
+
28
+ export let dom = {
29
+ /**
30
+ * Creates a real DOM tree from `vnode`. The [[Projection]] object returned will contain the resulting DOM Node in
31
+ * its [[Projection.domNode|domNode]] property.
32
+ * This is a low-level method. Users will typically use a [[Projector]] instead.
33
+ * @param vnode - The root of the virtual DOM tree that was created using the [[h]] function. NOTE: [[VNode]]
34
+ * objects may only be rendered once.
35
+ * @param projectionOptions - Options to be used to create and update the projection.
36
+ * @returns The [[Projection]] which also contains the DOM Node that was created.
37
+ */
38
+ create: (vnode: VNode, projectionOptions?: ProjectionOptions): Projection => {
39
+ projectionOptions = applyDefaultProjectionOptions(projectionOptions);
40
+ createDom(vnode, document.createElement("div"), undefined, projectionOptions);
41
+ return createProjection(vnode, projectionOptions);
42
+ },
43
+
44
+ /**
45
+ * Appends a new child node to the DOM which is generated from a [[VNode]].
46
+ * This is a low-level method. Users will typically use a [[Projector]] instead.
47
+ * @param parentNode - The parent node for the new child node.
48
+ * @param vnode - The root of the virtual DOM tree that was created using the [[h]] function. NOTE: [[VNode]]
49
+ * objects may only be rendered once.
50
+ * @param projectionOptions - Options to be used to create and update the [[Projection]].
51
+ * @returns The [[Projection]] that was created.
52
+ */
53
+ append: (
54
+ parentNode: Element,
55
+ vnode: VNode,
56
+ projectionOptions?: ProjectionOptions
57
+ ): Projection => {
58
+ projectionOptions = applyDefaultProjectionOptions(projectionOptions);
59
+ createDom(vnode, parentNode, undefined, projectionOptions);
60
+ return createProjection(vnode, projectionOptions);
61
+ },
62
+
63
+ /**
64
+ * Inserts a new DOM node which is generated from a [[VNode]].
65
+ * This is a low-level method. Users wil typically use a [[Projector]] instead.
66
+ * @param beforeNode - The node that the DOM Node is inserted before.
67
+ * @param vnode - The root of the virtual DOM tree that was created using the [[h]] function.
68
+ * NOTE: [[VNode]] objects may only be rendered once.
69
+ * @param projectionOptions - Options to be used to create and update the projection, see [[createProjector]].
70
+ * @returns The [[Projection]] that was created.
71
+ */
72
+ insertBefore: (
73
+ beforeNode: Element,
74
+ vnode: VNode,
75
+ projectionOptions?: ProjectionOptions
76
+ ): Projection => {
77
+ projectionOptions = applyDefaultProjectionOptions(projectionOptions);
78
+ createDom(vnode, beforeNode.parentNode!, beforeNode, projectionOptions);
79
+ return createProjection(vnode, projectionOptions);
80
+ },
81
+
82
+ /**
83
+ * Merges a new DOM node which is generated from a [[VNode]] with an existing DOM Node.
84
+ * This means that the virtual DOM and the real DOM will have one overlapping element.
85
+ * Therefore the selector for the root [[VNode]] will be ignored, but its properties and children will be applied to the Element provided.
86
+ * This is a low-level method. Users wil typically use a [[Projector]] instead.
87
+ * @param element - The existing element to adopt as the root of the new virtual DOM. Existing attributes and child nodes are preserved.
88
+ * @param vnode - The root of the virtual DOM tree that was created using the [[h]] function. NOTE: [[VNode]] objects
89
+ * may only be rendered once.
90
+ * @param projectionOptions - Options to be used to create and update the projection, see [[createProjector]].
91
+ * @returns The [[Projection]] that was created.
92
+ */
93
+ merge: (element: Element, vnode: VNode, projectionOptions?: ProjectionOptions): Projection => {
94
+ projectionOptions = applyDefaultProjectionOptions(projectionOptions);
95
+ vnode.domNode = element;
96
+ initPropertiesAndChildren(element, vnode, projectionOptions);
97
+ return createProjection(vnode, projectionOptions);
98
+ },
99
+
100
+ /**
101
+ * Replaces an existing DOM node with a node generated from a [[VNode]].
102
+ * This is a low-level method. Users will typically use a [[Projector]] instead.
103
+ * @param element - The node for the [[VNode]] to replace.
104
+ * @param vnode - The root of the virtual DOM tree that was created using the [[h]] function. NOTE: [[VNode]]
105
+ * objects may only be rendered once.
106
+ * @param projectionOptions - Options to be used to create and update the [[Projection]].
107
+ * @returns The [[Projection]] that was created.
108
+ */
109
+ replace: (element: Element, vnode: VNode, projectionOptions?: ProjectionOptions): Projection => {
110
+ projectionOptions = applyDefaultProjectionOptions(projectionOptions);
111
+ createDom(vnode, element.parentNode!, element, projectionOptions);
112
+ element.parentNode!.removeChild(element);
113
+ return createProjection(vnode, projectionOptions);
114
+ },
115
+ };
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Tests for Maquette h (hyperscript) module
3
+ */
4
+
5
+ import { describe, it, expect } from 'vitest';
6
+ import { h } from './h.js';
7
+
8
+ describe('Maquette h (hyperscript)', () => {
9
+ describe('Basic VNode creation', () => {
10
+ it('should create VNode with selector only', () => {
11
+ const vnode = h('div');
12
+
13
+ expect(vnode.vnodeSelector).toBe('div');
14
+ expect(vnode.properties).toBeUndefined();
15
+ expect(vnode.children).toBeUndefined();
16
+ expect(vnode.text).toBeUndefined();
17
+ });
18
+
19
+ it('should create VNode with properties', () => {
20
+ const vnode = h('div', { id: 'test', class: 'my-class' });
21
+
22
+ expect(vnode.vnodeSelector).toBe('div');
23
+ expect(vnode.properties).toEqual({ id: 'test', class: 'my-class' });
24
+ });
25
+
26
+ it('should create VNode with children array', () => {
27
+ const vnode = h('div', [h('span', ['text'])]);
28
+
29
+ expect(vnode.vnodeSelector).toBe('div');
30
+ expect(vnode.children).toHaveLength(1);
31
+ expect(vnode.children?.[0].vnodeSelector).toBe('span');
32
+ });
33
+ });
34
+
35
+ describe('Nested and complex children', () => {
36
+ it('should flatten nested arrays in children', () => {
37
+ const vnode = h('div', [
38
+ [h('span', ['a']), h('span', ['b'])],
39
+ h('span', ['c'])
40
+ ]);
41
+
42
+ expect(vnode.children).toHaveLength(3);
43
+ expect(vnode.children?.[0].vnodeSelector).toBe('span');
44
+ expect(vnode.children?.[1].vnodeSelector).toBe('span');
45
+ expect(vnode.children?.[2].vnodeSelector).toBe('span');
46
+ });
47
+
48
+ it('should handle deeply nested arrays', () => {
49
+ const vnode = h('div', [
50
+ [[h('span', ['level1'])]],
51
+ [[[h('span', ['level2'])]]]
52
+ ]);
53
+
54
+ expect(vnode.children).toHaveLength(2);
55
+ });
56
+
57
+ it('should convert string children to text VNodes in nested arrays', () => {
58
+ const vnode = h('div', [['text1', 'text2'], ['text3']]);
59
+
60
+ expect(vnode.children).toHaveLength(3);
61
+ vnode.children?.forEach(child => {
62
+ expect(child.vnodeSelector).toBe('');
63
+ expect(typeof child.text).toBe('string');
64
+ });
65
+ });
66
+
67
+ it('should filter out null, undefined, and false from children', () => {
68
+ const vnode = h('div', [
69
+ null,
70
+ undefined,
71
+ false,
72
+ h('span', ['visible'])
73
+ ]);
74
+
75
+ expect(vnode.children).toHaveLength(1);
76
+ expect(vnode.children?.[0].vnodeSelector).toBe('span');
77
+ });
78
+
79
+ it('should return undefined for empty children array', () => {
80
+ const vnode = h('div', [null, undefined, false]);
81
+
82
+ expect(vnode.children).toBeUndefined();
83
+ });
84
+ });
85
+
86
+ describe('Text nodes', () => {
87
+ it('should handle single text child as text property', () => {
88
+ const vnode = h('div', ['single text']);
89
+
90
+ expect(vnode.text).toBe('single text');
91
+ expect(vnode.children).toBeUndefined();
92
+ });
93
+
94
+ it('should convert empty string to undefined', () => {
95
+ const vnode = h('div', ['']);
96
+
97
+ expect(vnode.text).toBeUndefined();
98
+ });
99
+
100
+ it('should handle number strings in nested arrays', () => {
101
+ const vnode = h('div', [['123'], ['456']]);
102
+
103
+ expect(vnode.children).toHaveLength(2);
104
+ expect(vnode.children?.[0].text).toBe('123');
105
+ expect(vnode.children?.[1].text).toBe('456');
106
+ });
107
+ });
108
+
109
+ describe('Properties handling', () => {
110
+ it('should treat array as children when used as second argument', () => {
111
+ const vnode = h('div', [h('span')]);
112
+
113
+ expect(vnode.properties).toBeUndefined();
114
+ expect(vnode.children).toHaveLength(1);
115
+ });
116
+
117
+ it('should handle properties and children together', () => {
118
+ const vnode = h('div', { id: 'test' }, [h('span')]);
119
+
120
+ expect(vnode.properties?.id).toBe('test');
121
+ expect(vnode.children).toHaveLength(1);
122
+ });
123
+ });
124
+
125
+ describe('Error handling', () => {
126
+ it('should throw error when properties is a string', () => {
127
+ expect(() => {
128
+ h('div', 'invalid' as any);
129
+ }).toThrow('h called with invalid arguments');
130
+ });
131
+
132
+ it('should throw error when properties is a VNode', () => {
133
+ expect(() => {
134
+ h('div', h('span') as any);
135
+ }).toThrow('h called with invalid arguments');
136
+ });
137
+
138
+ it('should throw error when children contains VNode without array', () => {
139
+ expect(() => {
140
+ h('div', {}, h('span') as any);
141
+ }).toThrow('h called with invalid arguments');
142
+ });
143
+
144
+ it('should throw error when children is a string outside array', () => {
145
+ expect(() => {
146
+ h('div', {}, 'invalid' as any);
147
+ }).toThrow('h called with invalid arguments');
148
+ });
149
+ });
150
+
151
+ describe('Complex selectors', () => {
152
+ it('should handle selector with classes and id', () => {
153
+ const vnode = h('div.class1.class2#myid');
154
+
155
+ expect(vnode.vnodeSelector).toBe('div.class1.class2#myid');
156
+ });
157
+
158
+ it('should handle SVG elements', () => {
159
+ const vnode = h('svg', [h('circle', { r: 10 })]);
160
+
161
+ expect(vnode.vnodeSelector).toBe('svg');
162
+ expect(vnode.children?.[0].vnodeSelector).toBe('circle');
163
+ });
164
+ });
165
+ });