decue 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,396 @@
1
+ import { expect } from '@esm-bundle/chai';
2
+ import { createTemplate, createElement, getContentRoot } from './test-helpers.js';
3
+
4
+ // Unique counter for element names
5
+ let elementCounter = 0;
6
+ function uniqueName(prefix) {
7
+ return `${prefix}-${++elementCounter}`;
8
+ }
9
+
10
+ // Clean up after each test
11
+ afterEach(() => {
12
+ // Remove all test templates and elements
13
+ document.querySelectorAll('template[decue]').forEach(el => el.remove());
14
+ document.querySelectorAll('[data-test-element]').forEach(el => el.remove());
15
+ });
16
+
17
+ describe('placeholders', () => {
18
+
19
+ // Execute each test for all three custom element variants:
20
+ // 1. No shadow DOM
21
+ // 2. Shadow DOM open
22
+ // 3. Shadow DOM closed
23
+ ['none', 'open', 'closed'].forEach(shadow => {
24
+ describe("shadowmode: " + shadow, function() {
25
+ it('element with no attributes leaves placeholders unchanged', function () {
26
+ const name = uniqueName('test-placeholder-unchanged');
27
+ createTemplate(name, '<div data-value="{myattr}">Text with {myattr}</div>', { shadow: shadow !== 'none' ? shadow : undefined });
28
+
29
+ const el = createElement(name, {
30
+ 'data-test-element': '',
31
+ ...(shadow !== 'none' ? { shadow } : {})
32
+ });
33
+
34
+ // For closed shadow, verify element exists and is connected
35
+ expect(el).to.exist;
36
+ expect(el.isConnected).to.be.true;
37
+
38
+ const root = getContentRoot(el, shadow);
39
+ if (shadow !== 'closed') {
40
+ const div = root.querySelector('div');
41
+ expect(div).to.exist;
42
+ // Placeholder should remain unchanged when attribute is not set
43
+ expect(div.getAttribute('data-value')).to.equal('{myattr}');
44
+ expect(div.textContent).to.equal('Text with {myattr}');
45
+ } else {
46
+ // For closed shadow, verify the element responds to the attribute
47
+ // by checking that the attribute is in observedAttributes
48
+ const elementClass = customElements.get(name);
49
+ expect(elementClass.observedAttributes).to.include('myattr');
50
+ }
51
+ });
52
+
53
+ it('attribute placeholder is replaced when attribute is provided', function () {
54
+ const name = uniqueName('test-attribute-placeholder-replaced');
55
+ createTemplate(name, '<div data-value="{myattr}">Text with {myattr}</div>', { shadow: shadow !== 'none' ? shadow : undefined });
56
+
57
+ const el = createElement(name, {
58
+ 'data-test-element': '',
59
+ myattr: 'Hello World',
60
+ ...(shadow !== 'none' ? { shadow } : {})
61
+ });
62
+
63
+ // Test that attribute changes trigger events for closed shadow too
64
+ let attributeChanged = false;
65
+ if (shadow === 'closed') {
66
+ el.addEventListener('attributechange', () => {
67
+ attributeChanged = true;
68
+ });
69
+ }
70
+
71
+ const root = getContentRoot(el, shadow);
72
+ if (shadow !== 'closed') {
73
+ const div = root.querySelector('div');
74
+ expect(div).to.exist;
75
+ // Placeholder should be replaced with attribute value
76
+ expect(div.getAttribute('data-value')).to.equal('Hello World');
77
+ expect(div.textContent).to.equal('Text with Hello World');
78
+ } else {
79
+ // For closed shadow, test that the element responds to attribute changes
80
+ el.setAttribute('myattr', 'Updated Value');
81
+ expect(attributeChanged).to.be.true;
82
+ expect(el.getAttribute('myattr')).to.equal('Updated Value');
83
+ }
84
+ });
85
+
86
+ it('text content placeholder is replaced', function () {
87
+ const name = uniqueName('test-text-placeholder');
88
+ createTemplate(name, '<div class="content">Hello {name}!</div>', { shadow: shadow !== 'none' ? shadow : undefined });
89
+
90
+ const el = createElement(name, {
91
+ 'data-test-element': '',
92
+ name: 'World',
93
+ ...(shadow !== 'none' ? { shadow } : {})
94
+ });
95
+
96
+ // Verify element was created
97
+ expect(el).to.exist;
98
+
99
+ const root = getContentRoot(el, shadow);
100
+ if (shadow !== 'closed') {
101
+ const div = root.querySelector('.content');
102
+ expect(div).to.exist;
103
+ // Placeholder in text content should be replaced
104
+ expect(div.textContent).to.equal('Hello World!');
105
+ } else {
106
+ // For closed shadow, verify element exists and is connected
107
+ expect(el.shadowRoot).to.be.null;
108
+ expect(el.isConnected).to.be.true;
109
+ }
110
+ });
111
+
112
+ it('multiple placeholders are all replaced', function () {
113
+ const name = uniqueName('test-multiple-placeholders');
114
+ createTemplate(name, '<div data-greeting="{greeting}" data-name="{name}">{greeting} {name}!</div>', { shadow: shadow !== 'none' ? shadow : undefined });
115
+
116
+ const el = createElement(name, {
117
+ 'data-test-element': '',
118
+ greeting: 'Hello',
119
+ name: 'World',
120
+ ...(shadow !== 'none' ? { shadow } : {})
121
+ });
122
+
123
+ expect(el).to.exist;
124
+
125
+ const root = getContentRoot(el, shadow);
126
+ if (shadow !== 'closed') {
127
+ const div = root.querySelector('div');
128
+ expect(div).to.exist;
129
+ expect(div.getAttribute('data-greeting')).to.equal('Hello');
130
+ expect(div.getAttribute('data-name')).to.equal('World');
131
+ expect(div.textContent).to.equal('Hello World!');
132
+ } else {
133
+ expect(el.shadowRoot).to.be.null;
134
+ expect(el.isConnected).to.be.true;
135
+ }
136
+ });
137
+
138
+ it('placeholders update when attributes change', function () {
139
+ const name = uniqueName('test-placeholder-update');
140
+ createTemplate(name, '<div class="message">Message: {greeting}</div>', { shadow: shadow !== 'none' ? shadow : undefined });
141
+
142
+ // Create element
143
+ const el = createElement(name, {
144
+ 'data-test-element': '',
145
+ greeting: 'Hello',
146
+ ...(shadow !== 'none' ? { shadow } : {})
147
+ });
148
+
149
+ // Verify element was created
150
+ expect(el).to.exist;
151
+
152
+ const root = getContentRoot(el, shadow);
153
+ if (shadow !== 'closed') {
154
+ const div = root.querySelector('.message');
155
+ expect(div).to.exist;
156
+ // Initial placeholder should be replaced
157
+ expect(div.textContent).to.equal('Message: Hello');
158
+
159
+ // Update attribute
160
+ el.setAttribute('greeting', 'Hi there');
161
+
162
+ // Text content should update synchronously
163
+ expect(div.textContent).to.equal('Message: Hi there');
164
+
165
+ // Update again
166
+ el.setAttribute('greeting', 'Goodbye');
167
+ expect(div.textContent).to.equal('Message: Goodbye');
168
+ } else {
169
+ // For closed shadow, verify element exists and responds to attribute changes
170
+ expect(el.shadowRoot).to.be.null;
171
+ expect(el.isConnected).to.be.true;
172
+
173
+ // Test that attribute changes trigger events
174
+ let attributeChanged = false;
175
+ el.addEventListener('attributechange', () => {
176
+ attributeChanged = true;
177
+ });
178
+
179
+ el.setAttribute('greeting', 'Updated');
180
+ expect(attributeChanged).to.be.true;
181
+ }
182
+ });
183
+
184
+ it('attribute placeholders update when attributes change', function () {
185
+ const name = uniqueName('test-attribute-placeholder-update');
186
+ createTemplate(name, '<div class="content" data-value="{myattr}">Value: {myattr}</div>', { shadow: shadow !== 'none' ? shadow : undefined });
187
+
188
+ const el = createElement(name, {
189
+ 'data-test-element': '',
190
+ myattr: 'initial',
191
+ ...(shadow !== 'none' ? { shadow } : {})
192
+ });
193
+
194
+ expect(el).to.exist;
195
+
196
+ const root = getContentRoot(el, shadow);
197
+ if (shadow !== 'closed') {
198
+ const div = root.querySelector('.content');
199
+ expect(div).to.exist;
200
+ expect(div.getAttribute('data-value')).to.equal('initial');
201
+ expect(div.textContent).to.equal('Value: initial');
202
+
203
+ // Update attribute
204
+ el.setAttribute('myattr', 'updated');
205
+ expect(div.getAttribute('data-value')).to.equal('updated');
206
+ expect(div.textContent).to.equal('Value: updated');
207
+
208
+ // Update again
209
+ el.setAttribute('myattr', 'final');
210
+ expect(div.getAttribute('data-value')).to.equal('final');
211
+ expect(div.textContent).to.equal('Value: final');
212
+ } else {
213
+ expect(el.shadowRoot).to.be.null;
214
+ expect(el.isConnected).to.be.true;
215
+ }
216
+ });
217
+
218
+ it('placeholder with property access on attribute value', function () {
219
+ const name = uniqueName('test-property-access');
220
+ createTemplate(name, '<div class="content">Length: {text.length}</div>', { shadow: shadow !== 'none' ? shadow : undefined });
221
+
222
+ const el = createElement(name, {
223
+ 'data-test-element': '',
224
+ text: 'hello',
225
+ ...(shadow !== 'none' ? { shadow } : {})
226
+ });
227
+
228
+ expect(el).to.exist;
229
+
230
+ const root = getContentRoot(el, shadow);
231
+ if (shadow !== 'closed') {
232
+ const div = root.querySelector('.content');
233
+ expect(div).to.exist;
234
+ expect(div.textContent).to.equal('Length: 5');
235
+
236
+ // Update attribute
237
+ el.setAttribute('text', 'longer text');
238
+ expect(div.textContent).to.equal('Length: 11');
239
+ } else {
240
+ expect(el.shadowRoot).to.be.null;
241
+ expect(el.isConnected).to.be.true;
242
+ }
243
+ });
244
+
245
+ it('placeholder with method call on attribute value', function () {
246
+ const name = uniqueName('test-method-call');
247
+ createTemplate(name, '<div class="content">Upper: {text.toUpperCase}</div>', { shadow: shadow !== 'none' ? shadow : undefined });
248
+
249
+ const el = createElement(name, {
250
+ 'data-test-element': '',
251
+ text: 'hello world',
252
+ ...(shadow !== 'none' ? { shadow } : {})
253
+ });
254
+
255
+ expect(el).to.exist;
256
+
257
+ const root = getContentRoot(el, shadow);
258
+ if (shadow !== 'closed') {
259
+ const div = root.querySelector('.content');
260
+ expect(div).to.exist;
261
+ expect(div.textContent).to.equal('Upper: HELLO WORLD');
262
+
263
+ // Update attribute
264
+ el.setAttribute('text', 'goodbye');
265
+ expect(div.textContent).to.equal('Upper: GOODBYE');
266
+ } else {
267
+ expect(el.shadowRoot).to.be.null;
268
+ expect(el.isConnected).to.be.true;
269
+ }
270
+ });
271
+
272
+ it('placeholder with chained methods', function () {
273
+ const name = uniqueName('test-chained-methods');
274
+ createTemplate(name, '<div class="content">{text.trim.toUpperCase}</div>', { shadow: shadow !== 'none' ? shadow : undefined });
275
+
276
+ const el = createElement(name, {
277
+ 'data-test-element': '',
278
+ text: ' hello ',
279
+ ...(shadow !== 'none' ? { shadow } : {})
280
+ });
281
+
282
+ expect(el).to.exist;
283
+
284
+ const root = getContentRoot(el, shadow);
285
+ if (shadow !== 'closed') {
286
+ const div = root.querySelector('.content');
287
+ expect(div).to.exist;
288
+ expect(div.textContent).to.equal('HELLO');
289
+
290
+ // Update attribute
291
+ el.setAttribute('text', ' world ');
292
+ expect(div.textContent).to.equal('WORLD');
293
+ } else {
294
+ expect(el.shadowRoot).to.be.null;
295
+ expect(el.isConnected).to.be.true;
296
+ }
297
+ });
298
+
299
+ it('placeholder with element property reference', function () {
300
+ const name = uniqueName('test-element-property');
301
+ createTemplate(name, '<div class="content">Tag: {.tagName}</div>', { shadow: shadow !== 'none' ? shadow : undefined });
302
+
303
+ const el = createElement(name, {
304
+ 'data-test-element': '',
305
+ ...(shadow !== 'none' ? { shadow } : {})
306
+ });
307
+
308
+ expect(el).to.exist;
309
+
310
+ const root = getContentRoot(el, shadow);
311
+ if (shadow !== 'closed') {
312
+ const div = root.querySelector('.content');
313
+ expect(div).to.exist;
314
+ expect(div.textContent).to.equal(`Tag: ${name.toUpperCase()}`);
315
+ } else {
316
+ expect(el.shadowRoot).to.be.null;
317
+ expect(el.isConnected).to.be.true;
318
+ }
319
+ });
320
+
321
+ it('placeholder mixing registered function with method', function () {
322
+ const name = uniqueName('test-mixed-func-method');
323
+
324
+ // Register a function
325
+ decue.registeredFunctions['addPrefix'] = function(value) {
326
+ return 'PREFIX_' + value;
327
+ };
328
+
329
+ createTemplate(name, '<div class="content">{text|addPrefix.toUpperCase}</div>', { shadow: shadow !== 'none' ? shadow : undefined });
330
+
331
+ const el = createElement(name, {
332
+ 'data-test-element': '',
333
+ text: 'test',
334
+ ...(shadow !== 'none' ? { shadow } : {})
335
+ });
336
+
337
+ expect(el).to.exist;
338
+
339
+ const root = getContentRoot(el, shadow);
340
+ if (shadow !== 'closed') {
341
+ const div = root.querySelector('.content');
342
+ expect(div).to.exist;
343
+ expect(div.textContent).to.equal('PREFIX_TEST');
344
+
345
+ // Update attribute
346
+ el.setAttribute('text', 'data');
347
+ expect(div.textContent).to.equal('PREFIX_DATA');
348
+ } else {
349
+ expect(el.shadowRoot).to.be.null;
350
+ expect(el.isConnected).to.be.true;
351
+ }
352
+
353
+ // Cleanup
354
+ delete decue.registeredFunctions['addPrefix'];
355
+ });
356
+
357
+ it('placeholder with complex pipe chain', function () {
358
+ const name = uniqueName('test-complex-chain');
359
+
360
+ // Register multiple functions
361
+ decue.registeredFunctions['wrap'] = function(value) {
362
+ return `[${value}]`;
363
+ };
364
+
365
+ decue.registeredFunctions['double'] = function(value) {
366
+ return value + value;
367
+ };
368
+
369
+ createTemplate(name, '<div class="content">{text.trim|wrap|double.toUpperCase}</div>', { shadow: shadow !== 'none' ? shadow : undefined });
370
+
371
+ const el = createElement(name, {
372
+ 'data-test-element': '',
373
+ text: ' abc ',
374
+ ...(shadow !== 'none' ? { shadow } : {})
375
+ });
376
+
377
+ expect(el).to.exist;
378
+
379
+ const root = getContentRoot(el, shadow);
380
+ if (shadow !== 'closed') {
381
+ const div = root.querySelector('.content');
382
+ expect(div).to.exist;
383
+ // " abc " -> trim -> "abc" -> wrap -> "[abc]" -> double -> "[abc][abc]" -> toUpperCase -> "[ABC][ABC]"
384
+ expect(div.textContent).to.equal('[ABC][ABC]');
385
+ } else {
386
+ expect(el.shadowRoot).to.be.null;
387
+ expect(el.isConnected).to.be.true;
388
+ }
389
+
390
+ // Cleanup
391
+ delete decue.registeredFunctions['wrap'];
392
+ delete decue.registeredFunctions['double'];
393
+ });
394
+ });
395
+ });
396
+ });
@@ -0,0 +1,131 @@
1
+ import { expect } from '@esm-bundle/chai';
2
+ import { createTemplate, createElement, getContentRoot } from './test-helpers.js';
3
+
4
+ // Unique counter for element names
5
+ let elementCounter = 0;
6
+ function uniqueName(prefix) {
7
+ return `${prefix}-${++elementCounter}`;
8
+ }
9
+
10
+ // Clean up after each test
11
+ afterEach(() => {
12
+ // Remove all test templates and elements
13
+ document.querySelectorAll('template[decue]').forEach(el => el.remove());
14
+ document.querySelectorAll('[data-test-element]').forEach(el => el.remove());
15
+ });
16
+
17
+ describe('predefined', () => {
18
+
19
+ // Execute each test for all three custom element variants:
20
+ // 1. No shadow DOM
21
+ // 2. Shadow DOM open
22
+ // 3. Shadow DOM closed
23
+ ['none', 'open', 'closed'].forEach(shadow => {
24
+ describe("shadowmode: " + shadow, function() {
25
+ it('predefined elements work', function () {
26
+ const name = uniqueName('test-predefined');
27
+
28
+ // Define element first (before template exists)
29
+ decue.defineElement(false, name, null, false, []);
30
+
31
+ // Verify the custom element is defined
32
+ expect(customElements.get(name)).to.exist;
33
+
34
+ // The element class should be registered
35
+ const elementClass = customElements.get(name);
36
+ expect(elementClass).to.be.a('function');
37
+ expect(elementClass.observedAttributes).to.be.an('array');
38
+ });
39
+
40
+ it('observed attributes can be declared on the script element', function () {
41
+ const name = uniqueName('test-predefined-observed');
42
+
43
+ // Define element with explicit observed attributes
44
+ decue.defineElement(false, name, null, false, ['greeting', 'name']);
45
+
46
+ // Verify the custom element is defined with correct observedAttributes
47
+ const elementClass = customElements.get(name);
48
+ expect(elementClass).to.exist;
49
+ expect(elementClass.observedAttributes).to.include('greeting');
50
+ expect(elementClass.observedAttributes).to.include('name');
51
+
52
+ // Verify the attributes are in the array
53
+ expect(elementClass.observedAttributes.length).to.be.at.least(2);
54
+ });
55
+
56
+ it('attributes get observed (with MutationObserver) even if not declared on the script element', function (done) {
57
+ const name = uniqueName('test-predefined-mutation');
58
+
59
+ // Define element WITHOUT declaring observed attributes initially
60
+ decue.defineElement(false, name, null, false, []);
61
+
62
+ // Verify the element is defined
63
+ const elementClass = customElements.get(name);
64
+ expect(elementClass).to.exist;
65
+
66
+ // observedAttributes should be an empty array initially since no template has been processed
67
+ // and no attributes were explicitly declared
68
+ expect(elementClass.observedAttributes).to.be.an('array');
69
+ expect(elementClass.observedAttributes.length).to.equal(0);
70
+
71
+ // Create template with placeholder BEFORE creating the element
72
+ const template = document.createElement('template');
73
+ template.setAttribute('decue', name);
74
+ template.innerHTML = '<div class="content">Message: {message}</div><slot></slot>';
75
+ document.body.insertBefore(template, document.body.firstChild);
76
+
77
+ // Create element instance with attribute
78
+ const el = createElement(name, {
79
+ 'data-test-element': '',
80
+ message: 'Initial',
81
+ ...(shadow !== 'none' ? { shadow } : {})
82
+ }, '<span>some content</span>');
83
+
84
+ const root = getContentRoot(el, shadow);
85
+ if (shadow !== 'closed') {
86
+ const div = root.querySelector('.content');
87
+ expect(div).to.exist;
88
+ expect(div.textContent).to.equal('Message: Initial');
89
+
90
+ // Update attribute - should be observed via MutationObserver
91
+ // since 'message' was not in observedAttributes
92
+ el.setAttribute('message', 'Updated');
93
+
94
+ // Wait for MutationObserver to trigger update
95
+ setTimeout(() => {
96
+ expect(div.textContent).to.equal('Message: Updated');
97
+
98
+ // Update again to verify it continues working
99
+ el.setAttribute('message', 'Final');
100
+
101
+ setTimeout(() => {
102
+ expect(div.textContent).to.equal('Message: Final');
103
+
104
+ template.remove();
105
+ done();
106
+ }, 50);
107
+ }, 500);
108
+ } else {
109
+ // For closed shadow, verify element exists and responds to changes
110
+ expect(el.shadowRoot).to.be.null;
111
+ expect(el.isConnected).to.be.true;
112
+
113
+ // Test that attribute changes trigger events
114
+ let attributeChangeCount = 0;
115
+ el.addEventListener('attributechange', () => {
116
+ attributeChangeCount++;
117
+ });
118
+
119
+ el.setAttribute('message', 'Updated');
120
+
121
+ setTimeout(() => {
122
+ expect(attributeChangeCount).to.be.at.least(1);
123
+
124
+ template.remove();
125
+ done();
126
+ }, 50);
127
+ }
128
+ });
129
+ });
130
+ });
131
+ });