p-elements-core 1.2.30 → 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 +12 -3
  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,351 @@
1
+ /**
2
+ * Tests for Maquette projector module
3
+ */
4
+
5
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
6
+ import { createProjector } from './projector.js';
7
+ import { h } from './h.js';
8
+
9
+ describe('Maquette projector', () => {
10
+ let container: HTMLDivElement;
11
+ let projectors: Array<ReturnType<typeof createProjector>> = [];
12
+
13
+ beforeEach(() => {
14
+ container = document.createElement('div');
15
+ document.body.appendChild(container);
16
+ projectors = [];
17
+ });
18
+
19
+ afterEach(() => {
20
+ // Stop all projectors to prevent async operations from interfering
21
+ projectors.forEach(p => {
22
+ try {
23
+ p.stop();
24
+ } catch (e) {
25
+ // Ignore errors if already stopped
26
+ }
27
+ });
28
+
29
+ if (container.parentNode) {
30
+ document.body.removeChild(container);
31
+ }
32
+ });
33
+
34
+ describe('createProjector', () => {
35
+ it('should create a projector instance', () => {
36
+ const projector = createProjector();
37
+
38
+ expect(projector).toBeDefined();
39
+ expect(projector.append).toBeDefined();
40
+ expect(projector.merge).toBeDefined();
41
+ expect(projector.insertBefore).toBeDefined();
42
+ expect(projector.replace).toBeDefined();
43
+ expect(projector.scheduleRender).toBeDefined();
44
+ expect(projector.renderNow).toBeDefined();
45
+ expect(projector.stop).toBeDefined();
46
+ expect(projector.resume).toBeDefined();
47
+ expect(projector.detach).toBeDefined();
48
+ });
49
+
50
+ it('should append VNode and render', () => {
51
+ const projector = createProjector();
52
+ let text = 'Hello';
53
+
54
+ projector.append(container, () => h('div', [text]));
55
+
56
+ expect(container.children.length).toBe(1);
57
+ expect(container.children[0].textContent).toBe('Hello');
58
+
59
+ text = 'Updated';
60
+ projector.renderNow();
61
+
62
+ expect(container.children[0].textContent).toBe('Updated');
63
+ });
64
+
65
+ it('should schedule render on next animation frame', () => {
66
+ const projector = createProjector();
67
+ let count = 1;
68
+
69
+ projector.append(container, () => h('div', [`Count: ${count}`]));
70
+ expect(container.children[0].textContent).toBe('Count: 1');
71
+
72
+ count = 2;
73
+ projector.scheduleRender();
74
+
75
+ return new Promise<void>((resolve) => {
76
+ requestAnimationFrame(() => {
77
+ expect(container.children[0].textContent).toBe('Count: 2');
78
+ projector.stop();
79
+ resolve();
80
+ });
81
+ });
82
+ });
83
+
84
+ it('should not schedule multiple renders', () => {
85
+ const projector = createProjector();
86
+ let renderCount = 0;
87
+
88
+ projector.append(container, () => {
89
+ renderCount++;
90
+ return h('div', [`Render ${renderCount}`]);
91
+ });
92
+
93
+ const initialRenderCount = renderCount;
94
+
95
+ // Multiple scheduleRender calls should only trigger one render
96
+ projector.scheduleRender();
97
+ projector.scheduleRender();
98
+ projector.scheduleRender();
99
+
100
+ return new Promise<void>((resolve) => {
101
+ setTimeout(() => {
102
+ // Should have rendered only once more after initial
103
+ expect(renderCount).toBe(initialRenderCount + 1);
104
+ projector.stop();
105
+ resolve();
106
+ }, 50);
107
+ });
108
+ });
109
+
110
+ it('should stop rendering', () => {
111
+ const projector = createProjector();
112
+ let count = 1;
113
+
114
+ projector.append(container, () => h('div', [`Count: ${count}`]));
115
+ projector.stop();
116
+
117
+ count = 2;
118
+ projector.scheduleRender();
119
+
120
+ return new Promise<void>((resolve) => {
121
+ setTimeout(() => {
122
+ // Should not have updated because projector is stopped
123
+ expect(container.children[0].textContent).toBe('Count: 1');
124
+ resolve();
125
+ }, 100);
126
+ });
127
+ });
128
+
129
+ it('should resume rendering after stop', () => {
130
+ const projector = createProjector();
131
+ let count = 1;
132
+
133
+ projector.append(container, () => h('div', [`Count: ${count}`]));
134
+ projector.stop();
135
+
136
+ count = 2;
137
+ projector.resume();
138
+
139
+ return new Promise<void>((resolve) => {
140
+ requestAnimationFrame(() => {
141
+ expect(container.children[0].textContent).toBe('Count: 2');
142
+ projector.stop();
143
+ resolve();
144
+ });
145
+ });
146
+ });
147
+
148
+ it('should merge with existing element', () => {
149
+ const existingDiv = document.createElement('div');
150
+ existingDiv.id = 'existing';
151
+ existingDiv.textContent = 'Original';
152
+ container.appendChild(existingDiv);
153
+
154
+ const projector = createProjector();
155
+ let mergedText = 'Merged';
156
+
157
+ projector.merge(existingDiv, () => h('div', [mergedText]));
158
+
159
+ expect(existingDiv.id).toBe('existing');
160
+ expect(existingDiv.textContent).toContain('Merged');
161
+ });
162
+
163
+ it('should insert before specified node', () => {
164
+ const firstChild = document.createElement('div');
165
+ firstChild.textContent = 'First';
166
+ container.appendChild(firstChild);
167
+
168
+ const lastChild = document.createElement('div');
169
+ lastChild.textContent = 'Last';
170
+ container.appendChild(lastChild);
171
+
172
+ const projector = createProjector();
173
+ projector.insertBefore(lastChild, () => h('div', ['Middle']));
174
+
175
+ expect(container.children.length).toBe(3);
176
+ expect(container.children[0].textContent).toBe('First');
177
+ expect(container.children[1].textContent).toBe('Middle');
178
+ expect(container.children[2].textContent).toBe('Last');
179
+ });
180
+
181
+ it('should replace element', () => {
182
+ const oldElement = document.createElement('div');
183
+ oldElement.textContent = 'Old';
184
+ container.appendChild(oldElement);
185
+
186
+ const projector = createProjector();
187
+ projector.replace(oldElement, () => h('span', ['New']));
188
+
189
+ expect(container.children.length).toBe(1);
190
+ expect(container.children[0].tagName).toBe('SPAN');
191
+ expect(container.children[0].textContent).toBe('New');
192
+ });
193
+
194
+ it('should handle multiple projections', () => {
195
+ const div1 = document.createElement('div');
196
+ const div2 = document.createElement('div');
197
+ container.appendChild(div1);
198
+ container.appendChild(div2);
199
+
200
+ const projector = createProjector();
201
+ let text1 = 'Projection 1';
202
+ let text2 = 'Projection 2';
203
+
204
+ projector.append(div1, () => h('span', [text1]));
205
+ projector.append(div2, () => h('span', [text2]));
206
+
207
+ expect(div1.children[0].textContent).toBe('Projection 1');
208
+ expect(div2.children[0].textContent).toBe('Projection 2');
209
+
210
+ text1 = 'Updated 1';
211
+ text2 = 'Updated 2';
212
+ projector.renderNow();
213
+
214
+ expect(div1.children[0].textContent).toBe('Updated 1');
215
+ expect(div2.children[0].textContent).toBe('Updated 2');
216
+ });
217
+
218
+ it('should detach render function', () => {
219
+ const projector = createProjector();
220
+ const renderFunc = () => h('div', ['Detachable']);
221
+
222
+ projector.append(container, renderFunc);
223
+ expect(container.children.length).toBe(1);
224
+
225
+ const detached = projector.detach(renderFunc);
226
+ expect(detached).toBeDefined();
227
+ });
228
+
229
+ it('should throw error when detaching non-existent render function', () => {
230
+ const projector = createProjector();
231
+ const renderFunc = () => h('div', ['Test']);
232
+
233
+ expect(() => {
234
+ projector.detach(renderFunc);
235
+ }).toThrow('renderFunction was not found');
236
+ });
237
+
238
+ it('should call performance logger', () => {
239
+ const logCalls: Array<{ type: string; detail: any }> = [];
240
+ const performanceLogger = (type: string, detail: any) => {
241
+ logCalls.push({ type, detail });
242
+ };
243
+
244
+ const projector = createProjector({ performanceLogger });
245
+ projector.append(container, () => h('div', ['Test']));
246
+ projector.renderNow();
247
+
248
+ expect(logCalls.length).toBeGreaterThan(0);
249
+ expect(logCalls.some(call => call.type === 'renderStart')).toBe(true);
250
+ });
251
+
252
+ it('should handle event handler interceptor', () => {
253
+ let clickCalled = false;
254
+ const handleClick = () => {
255
+ clickCalled = true;
256
+ };
257
+
258
+ const projector = createProjector();
259
+ projector.append(container, () =>
260
+ h('button', { onclick: handleClick }, ['Click me'])
261
+ );
262
+
263
+ const button = container.querySelector('button');
264
+ button?.click();
265
+
266
+ expect(clickCalled).toBe(true);
267
+ });
268
+
269
+ it('should trigger re-render when event handler is called', () => {
270
+ let count = 0;
271
+ let renderCount = 0;
272
+
273
+ const handleClick = () => {
274
+ count++;
275
+ };
276
+
277
+ const projector = createProjector();
278
+ projector.append(container, () => {
279
+ renderCount++;
280
+ return h('div', [
281
+ h('button', { onclick: handleClick }),
282
+ h('span', [`Count: ${count}`])
283
+ ]);
284
+ });
285
+
286
+ const initialRenderCount = renderCount;
287
+ const button = container.querySelector('button');
288
+
289
+ button?.click();
290
+
291
+ return new Promise<void>((resolve) => {
292
+ requestAnimationFrame(() => {
293
+ expect(renderCount).toBeGreaterThan(initialRenderCount);
294
+ expect(container.querySelector('span')?.textContent).toBe('Count: 1');
295
+ projector.stop();
296
+ resolve();
297
+ });
298
+ });
299
+ });
300
+
301
+ it('should handle nested event handlers', () => {
302
+ let outerClicked = false;
303
+ let innerClicked = false;
304
+
305
+ const projector = createProjector();
306
+ projector.append(container, () =>
307
+ h('div', { onclick: () => { outerClicked = true; } }, [
308
+ h('button', { onclick: () => { innerClicked = true; } }, ['Inner'])
309
+ ])
310
+ );
311
+
312
+ const button = container.querySelector('button');
313
+ button?.click();
314
+
315
+ expect(innerClicked).toBe(true);
316
+ });
317
+
318
+ it('should update multiple times', () => {
319
+ const projector = createProjector();
320
+ let value = 0;
321
+
322
+ projector.append(container, () => h('div', [`Value: ${value}`]));
323
+
324
+ for (let i = 1; i <= 5; i++) {
325
+ value = i;
326
+ projector.renderNow();
327
+ expect(container.children[0].textContent).toBe(`Value: ${i}`);
328
+ }
329
+ });
330
+
331
+ it('should handle complex VNode updates', () => {
332
+ const projector = createProjector();
333
+ let items = ['a', 'b', 'c'];
334
+
335
+ projector.append(container, () =>
336
+ h('ul', items.map(item => h('li', { key: item }, [item])))
337
+ );
338
+
339
+ expect(container.querySelectorAll('li').length).toBe(3);
340
+
341
+ items = ['a', 'c', 'd', 'e'];
342
+ projector.renderNow();
343
+
344
+ expect(container.querySelectorAll('li').length).toBe(4);
345
+ expect(container.querySelectorAll('li')[0].textContent).toBe('a');
346
+ expect(container.querySelectorAll('li')[1].textContent).toBe('c');
347
+ expect(container.querySelectorAll('li')[2].textContent).toBe('d');
348
+ expect(container.querySelectorAll('li')[3].textContent).toBe('e');
349
+ });
350
+ });
351
+ });