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.
- package/CHANGELOG.md +201 -0
- package/demo/sample.js +1 -1
- package/demo/theme.css +1 -0
- package/dist/p-elements-core-modern.js +1 -1
- package/dist/p-elements-core.js +1 -1
- package/index.html +15 -2
- package/p-elements-core.d.ts +11 -1
- package/package.json +10 -3
- package/src/custom-element-controller.test.ts +226 -0
- package/src/custom-element.test.ts +906 -0
- package/src/custom-element.ts +74 -17
- package/src/custom-style-element.ts +4 -1
- package/src/decorators/bind.test.ts +163 -0
- package/src/decorators/property.test.ts +279 -0
- package/src/decorators/property.ts +176 -10
- package/src/decorators/query.test.ts +146 -0
- package/src/helpers/css.test.ts +150 -0
- package/src/maquette/cache.test.ts +150 -0
- package/src/maquette/dom.test.ts +263 -0
- package/src/maquette/h.test.ts +165 -0
- package/src/maquette/mapping.test.ts +294 -0
- package/src/maquette/maquette.test.ts +493 -0
- package/src/maquette/projection.test.ts +366 -0
- package/src/maquette/projector.test.ts +351 -0
- package/src/maquette/projector.ts +6 -1
- package/src/sample/sample.tsx +167 -8
- package/src/test-setup.ts +85 -0
- package/src/test-utils.ts +223 -0
- package/tsconfig.json +1 -0
- package/vitest.config.ts +41 -0
- 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
|
+
});
|
|
@@ -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);
|
package/src/sample/sample.tsx
CHANGED
|
@@ -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
|
-
|
|
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"
|
|
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;
|