p-elements-core 1.2.32-rc1 → 1.2.32-rc11

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.
@@ -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
+ });
@@ -69,7 +69,12 @@ let createEventHandlerInterceptor = (
69
69
  // modified event handler
70
70
  return function(this: Node, evt: Event): boolean | undefined | void {
71
71
  performanceLogger("domEvent", evt);
72
- let projection = getProjection()!;
72
+ let projection = getProjection();
73
+ // Guard: projection may be undefined when an event handler is invoked synchronously
74
+ // during initial DOM creation (e.g. by a custom-element setter calling onchange).
75
+ if (!projection) {
76
+ return eventHandler.apply(this, arguments);
77
+ }
73
78
  let parentNodePath = createParentNodePath(evt.currentTarget as Element, projection.domNode);
74
79
  parentNodePath.reverse();
75
80
  let matchingVNode = findVNodeByParentNodePath(projection.getLastRender(), parentNodePath);
@@ -1,21 +1,80 @@
1
- // @ts-nocheck
2
1
  import { Highlightable } from "./mixin/highlight";
3
2
  import anime from "animejs";
4
3
 
5
- import {customElementConfig as CustomElementConfig} from "../decorators/custom-element-config";
4
+ import {customElementConfig, customElementConfig as CustomElementConfig} from "../decorators/custom-element-config";
6
5
  import {property as Property, AttributeConverter} from "../decorators/property";
7
6
  import {propertyRenderOnSet as PropertyRenderOnSet} from "../decorators/render-property-on-set";
8
7
  import {query as Query} from "../decorators/query";
9
8
  import {CustomElement} from "../custom-element";
10
9
  import {CustomElementController} from "../custom-element-controller";
11
10
 
11
+
12
+ @customElementConfig({tagName: "render-on-set"})
13
+ export class RenderOnSet extends CustomElement {
14
+
15
+ @PropertyRenderOnSet
16
+ public name: string = "foo";
17
+
18
+ render() {
19
+ return <div>Render on set: {this.name}</div>;
20
+ }
21
+
22
+ #initDone = false;
23
+
24
+ connectedCallback(): void {
25
+
26
+ if (this.#initDone) {
27
+ return
28
+ }
29
+ this.#initDone = true;
30
+ let template = this.templateFromString(`<style>div{color: lime}</style><div />`);
31
+ this.shadowRoot.appendChild(template);
32
+ this.createProjector(this.shadowRoot.querySelector("div"), this.render);
33
+ }
34
+
35
+ }
36
+
37
+
38
+ @CustomElementConfig({
39
+ tagName: "p-bugs-03",
40
+ })
41
+ export class PBugs03 extends CustomElement {
42
+ static style = `/* */`;
43
+
44
+ @PropertyRenderOnSet
45
+ private _zero: number = 0;
46
+
47
+ private _renderCount: number = 0;
48
+
49
+ private alwaysZero() {
50
+ return 0;
51
+ }
52
+
53
+ private get zero(): number {
54
+ this._zero = this.alwaysZero();
55
+ return this._zero;
56
+ }
57
+
58
+ // Bug: Updating a @RenderOnSet property with same value triggers useless re-render
59
+
60
+ render() {
61
+ this._renderCount++;
62
+
63
+ return (
64
+ <div>
65
+ Value of Zero: {this.zero} - Renders: {this._renderCount}
66
+ </div>
67
+ );
68
+ };
69
+ }
70
+
12
71
  @CustomElementConfig({
13
72
  tagName: "my-button",
14
73
  })
15
74
  export class MyButton extends CustomElement {
16
75
  static style = `.foo{background-color: red; color: white; border: 0px;}`;
17
76
  static delegatesFocus = true;
18
- render = () => {
77
+ render(){
19
78
  return <button class="foo"><slot></slot></button>;
20
79
  }
21
80
  }
@@ -23,7 +82,7 @@ export class MyButton extends CustomElement {
23
82
  @CustomElementConfig({
24
83
  tagName: "my-greetings",
25
84
  })
26
- export class MyGreetings extends Highlightable(class extends CustomElement { }) {
85
+ export class MyGreetings extends Highlightable(class extends CustomElement { render() { return <div></div> } }) {
27
86
 
28
87
  static isFormAssociated = true;
29
88
 
@@ -95,7 +154,7 @@ export class MyGreetings extends Highlightable(class extends CustomElement { })
95
154
  console.log({ e, p: this });
96
155
  };
97
156
 
98
- private render = () => {
157
+ render(){
99
158
  return (
100
159
  <div>
101
160
  <input type="checkbox" checked={this.name === "Peter"} />
@@ -140,8 +199,8 @@ export class MyGreetings extends Highlightable(class extends CustomElement { })
140
199
  afterCreate={this.peterCreate}
141
200
  class="is-peter"
142
201
  eventListeners={[["click", this.onPeterClick]]}
143
- enterAnimation={this.enterAnimation}
144
- exitAnimation={this.exitAnimation}
202
+ enterAnimation={(element, properties) => this.enterAnimation(element, properties)}
203
+ exitAnimation={(element, removeDomNodeFunction, properties) => this.exitAnimation(element, removeDomNodeFunction, properties)}
145
204
  >
146
205
  <p>Hello Peter</p>
147
206
  <img
@@ -166,6 +225,7 @@ export class MyGreetings extends Highlightable(class extends CustomElement { })
166
225
  }
167
226
 
168
227
  connectedCallback() {
228
+ super.connectedCallback();
169
229
  let template = this.templateFromString(`
170
230
  <style>
171
231
 
@@ -309,7 +369,7 @@ class PFoo extends CustomElement {
309
369
  @Property({ attribute: "items", type: "object", reflect: true, converter: stringArrayConverter })
310
370
  items: string[] = ["foo", "bar"];
311
371
 
312
- @Property({ type: "string", attribute: "nick-name", readonly: true })
372
+ @Property({ type: "string", attribute: "nick-name"})
313
373
  nickName: string = "de prutser";
314
374
 
315
375
  shouldUpdate(property, oldValue, newValue) {
@@ -360,9 +420,108 @@ class SuperAnchorElement extends HTMLAnchorElement {
360
420
  }
361
421
  }
362
422
  public connectedCallback() {
423
+
363
424
  this.classList.add("super");
364
425
  this.style.color = "red";
365
426
  }
366
427
  }
367
428
 
368
429
  window.customElements.define("super-a", SuperAnchorElement, { extends: "a" });
430
+
431
+
432
+ // Bring your own projector in non shadow root element
433
+ @CustomElementConfig({
434
+ tagName: "bring-your-own-projector",
435
+ })
436
+ export class BringYourOwnProjector extends CustomElement {
437
+
438
+ constructor() {
439
+ super();
440
+ this.initProjector();
441
+ }
442
+
443
+ init() {
444
+ console.log("This is a lifecycle for BringYourOwnProjector");
445
+ }
446
+
447
+ initProjector() {
448
+ this.rootElement = document.createElement("div");
449
+ this.rootElement.classList.add("root");
450
+ this.createProjector(this.rootElement, this.render);
451
+ console.log("init");
452
+ }
453
+
454
+ @PropertyRenderOnSet
455
+ name: string = "foo";
456
+
457
+ #_doneSOmething = false;
458
+
459
+ get doneSomething() {
460
+ return this.#_doneSOmething;
461
+ }
462
+
463
+ doItNow(value) {
464
+ this.#_doneSOmething = value;
465
+ this.renderNow();
466
+ }
467
+
468
+ rootElement: HTMLDivElement;
469
+
470
+ render() {
471
+ return <div>Hello {this.name} {this.doneSomething && <p>We did it!!</p>}</div>;
472
+ }
473
+
474
+ connectedCallback() {
475
+ this.appendChild(this.rootElement);
476
+ }
477
+
478
+ }
479
+
480
+ // Bring your own projector in non shadow root element
481
+ @CustomElementConfig({
482
+ tagName: "bring-your-own-projector-in-shadow-root",
483
+ })
484
+ export class BringYourOwnProjectorInShadowRoot extends CustomElement {
485
+
486
+ constructor() {
487
+ super();
488
+ this.initProjector();
489
+ }
490
+
491
+ init() {
492
+ console.log("This is a lifecycle for BringYourOwnProjector");
493
+ }
494
+
495
+ initProjector() {
496
+ this.attachShadow({ mode: "open" });
497
+ this.rootElement = document.createElement("div");
498
+ this.rootElement.classList.add("root");
499
+ this.createProjector(this.rootElement, this.render);
500
+ console.log("init");
501
+ }
502
+
503
+ @PropertyRenderOnSet
504
+ name: string = "foo";
505
+
506
+ #_doneSOmething = false;
507
+
508
+ get doneSomething() {
509
+ return this.#_doneSOmething;
510
+ }
511
+
512
+ doItNow(value) {
513
+ this.#_doneSOmething = value;
514
+ this.renderNow();
515
+ }
516
+
517
+ rootElement: HTMLDivElement;
518
+
519
+ render() {
520
+ return <div>Hello {this.name} {this.doneSomething && <p>We did it!!</p>}</div>;
521
+ }
522
+
523
+ connectedCallback() {
524
+ this.shadowRoot.appendChild(this.rootElement);
525
+ }
526
+
527
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Test setup file - Vitest configuration
3
+ * Configures polyfills and global test utilities
4
+ */
5
+
6
+ import { expect, afterEach, vi } from 'vitest';
7
+
8
+ // Import Maquette at the top level for type definitions
9
+ import * as Maquette from './maquette/index.js';
10
+
11
+ // Declare global Maquette if in browser environment
12
+ declare global {
13
+ interface Window {
14
+ Maquette: typeof Maquette;
15
+ }
16
+ }
17
+
18
+ // Only run setup code if we're in a browser environment
19
+ if (typeof window !== 'undefined') {
20
+ // Set up Maquette global
21
+ (window as any).Maquette = Maquette;
22
+
23
+ // Import and register CustomStyleElement for CSS @apply polyfill
24
+ if (typeof HTMLLinkElement !== 'undefined') {
25
+ import('./custom-style-element.js').catch(() => {
26
+ // Ignore if import fails in test environment
27
+ });
28
+ }
29
+ }
30
+
31
+ // Extend Chai matchers for better custom element testing
32
+ declare global {
33
+ namespace Chai {
34
+ interface Assertion {
35
+ shadowRoot: ShadowRoot;
36
+ }
37
+ }
38
+ }
39
+
40
+ // Store for tracking registered custom element names to prevent conflicts
41
+ const registeredElements = new Set<string>();
42
+ const elementsCreated: HTMLElement[] = [];
43
+
44
+ /**
45
+ * Generate a unique custom element tag name for testing
46
+ * Ensures no conflicts between test cases
47
+ */
48
+ export function generateUniqueTagName(prefix: string = 'test-element'): string {
49
+ const timestamp = Date.now();
50
+ const random = Math.random().toString(36).substr(2, 9);
51
+ const tagName = `${prefix}-${timestamp}-${random}`.toLowerCase();
52
+
53
+ registeredElements.add(tagName);
54
+ return tagName;
55
+ }
56
+
57
+ /**
58
+ * Clean up helper - unregister all test elements after tests
59
+ * Note: CustomElementRegistry doesn't support unregistration,
60
+ * so we track what was registered for uniqueness
61
+ */
62
+ export function resetRegisteredElements(): void {
63
+ registeredElements.clear();
64
+ }
65
+
66
+ /**
67
+ * Track created elements for cleanup
68
+ */
69
+ export function trackElement(el: HTMLElement): void {
70
+ elementsCreated.push(el);
71
+ }
72
+
73
+ /**
74
+ * Clean up all created elements after each test
75
+ */
76
+ afterEach(() => {
77
+ elementsCreated.forEach(el => {
78
+ if (el.parentElement) {
79
+ el.parentElement.removeChild(el);
80
+ }
81
+ });
82
+ elementsCreated.length = 0;
83
+ });
84
+ // Global test timeout for async operations
85
+ export const DEFAULT_TEST_TIMEOUT = 5000;