decue 1.0.0 → 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,464 @@
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('scriptAttributes', () => {
18
+
19
+ describe('defineElement API', function() {
20
+ it('can define regular elements via defineElement', function () {
21
+ const name = uniqueName('test-regular-element');
22
+
23
+ // Define element programmatically (simulates elements="" attribute)
24
+ decue.defineElement(false, name, undefined, false, []);
25
+
26
+ // Verify element is defined
27
+ const elementClass = customElements.get(name);
28
+ expect(elementClass).to.exist;
29
+ expect(elementClass.formAssociated).to.be.false;
30
+ });
31
+
32
+ it('can define form-associated elements via defineElement', function () {
33
+ const name = uniqueName('test-form-element');
34
+
35
+ // Define form-associated element programmatically (simulates form-associated="" attribute)
36
+ decue.defineElement(false, name, undefined, true, []);
37
+
38
+ // Verify element is defined
39
+ const elementClass = customElements.get(name);
40
+ expect(elementClass).to.exist;
41
+ expect(elementClass.formAssociated).to.be.true;
42
+ });
43
+
44
+ it('can define elements with observed attributes', function () {
45
+ const name = uniqueName('test-observed-attrs');
46
+
47
+ // Define element with explicit observed attributes
48
+ decue.defineElement(false, name, undefined, false, ['attr1', 'attr2']);
49
+
50
+ // Verify element is defined with correct observedAttributes
51
+ const elementClass = customElements.get(name);
52
+ expect(elementClass).to.exist;
53
+ expect(elementClass.observedAttributes).to.include('attr1');
54
+ expect(elementClass.observedAttributes).to.include('attr2');
55
+ });
56
+
57
+ it('can define multiple elements', function () {
58
+ const name1 = uniqueName('test-multi-1');
59
+ const name2 = uniqueName('test-multi-2');
60
+
61
+ // Define multiple elements
62
+ decue.defineElement(false, name1, undefined, false, []);
63
+ decue.defineElement(false, name2, undefined, false, []);
64
+
65
+ // Verify both are defined
66
+ expect(customElements.get(name1)).to.exist;
67
+ expect(customElements.get(name2)).to.exist;
68
+ });
69
+ });
70
+
71
+ describe('registeredFunctions', function() {
72
+ it('can register simple functions', function () {
73
+ const name = uniqueName('test-simple-func');
74
+
75
+ // Register a simple function
76
+ const testFunc = function(value) {
77
+ return value.toUpperCase();
78
+ };
79
+ decue.registeredFunctions['testSimpleFunc'] = testFunc;
80
+
81
+ // Create element using the registered function
82
+ createTemplate(name, '<div class="content">{text|testSimpleFunc}</div>');
83
+
84
+ const el = createElement(name, {
85
+ 'data-test-element': '',
86
+ text: 'hello'
87
+ });
88
+
89
+ const root = getContentRoot(el, 'none');
90
+ const div = root.querySelector('.content');
91
+ expect(div).to.exist;
92
+ expect(div.textContent).to.equal('HELLO');
93
+
94
+ // Cleanup
95
+ delete decue.registeredFunctions['testSimpleFunc'];
96
+ });
97
+
98
+ it('can register namespaced functions', function () {
99
+ const name = uniqueName('test-namespaced-func');
100
+
101
+ // Create a namespace with a function
102
+ window.MyTestLib = {
103
+ processText: function(value) {
104
+ return `[${value}]`;
105
+ }
106
+ };
107
+
108
+ // Register the namespaced function with a simple alias
109
+ decue.registeredFunctions['processText'] = window.MyTestLib.processText;
110
+
111
+ // Create element using the registered function
112
+ createTemplate(name, '<div class="content">{text|processText}</div>');
113
+
114
+ const el = createElement(name, {
115
+ 'data-test-element': '',
116
+ text: 'test'
117
+ });
118
+
119
+ const root = getContentRoot(el, 'none');
120
+ const div = root.querySelector('.content');
121
+ expect(div).to.exist;
122
+ expect(div.textContent).to.equal('[test]');
123
+
124
+ // Cleanup
125
+ delete decue.registeredFunctions['processText'];
126
+ delete window.MyTestLib;
127
+ });
128
+
129
+ it('can register aliased functions', function () {
130
+ const name = uniqueName('test-aliased-func');
131
+
132
+ // Create a function at a specific location
133
+ window.SomeLibrary = {
134
+ utilities: {
135
+ format: function(value) {
136
+ return `>>> ${value} <<<`;
137
+ }
138
+ }
139
+ };
140
+
141
+ // Register with an alias
142
+ decue.registeredFunctions['fmt'] = window.SomeLibrary.utilities.format;
143
+
144
+ // Create element using the alias
145
+ createTemplate(name, '<div class="content">{text|fmt}</div>');
146
+
147
+ const el = createElement(name, {
148
+ 'data-test-element': '',
149
+ text: 'aliased'
150
+ });
151
+
152
+ const root = getContentRoot(el, 'none');
153
+ const div = root.querySelector('.content');
154
+ expect(div).to.exist;
155
+ expect(div.textContent).to.equal('>>> aliased <<<');
156
+
157
+ // Cleanup
158
+ delete decue.registeredFunctions['fmt'];
159
+ delete window.SomeLibrary;
160
+ });
161
+
162
+ it('can register deeply nested function with alias (script attribute format)', function () {
163
+ const name = uniqueName('test-script-attr-alias');
164
+
165
+ // Create a deeply nested namespace structure
166
+ // This simulates: functions="myFunc:window.someNamespace.somePackage.someFunction"
167
+ window.someNamespace = {
168
+ somePackage: {
169
+ someFunction: function(value) {
170
+ return `PROCESSED: ${value}`;
171
+ }
172
+ }
173
+ };
174
+
175
+ // Simulate what the script attribute does: parse "myFunc:window.someNamespace.somePackage.someFunction"
176
+ const alias = 'myFunc';
177
+ const path = 'someNamespace.somePackage.someFunction';
178
+
179
+ // Resolve the function from the path (simulating the script attribute behavior)
180
+ const func = path.split('.').reduce(function(obj, prop) {
181
+ return obj[prop];
182
+ }, window);
183
+
184
+ // Register with the alias
185
+ decue.registeredFunctions[alias] = func;
186
+
187
+ // Verify registration
188
+ expect(decue.registeredFunctions[alias]).to.be.a('function');
189
+
190
+ // Create element using the registered alias
191
+ createTemplate(name, '<div class="content">{text|myFunc}</div>');
192
+
193
+ const el = createElement(name, {
194
+ 'data-test-element': '',
195
+ text: 'test-data'
196
+ });
197
+
198
+ const root = getContentRoot(el, 'none');
199
+ const div = root.querySelector('.content');
200
+ expect(div).to.exist;
201
+ expect(div.textContent).to.equal('PROCESSED: test-data');
202
+
203
+ // Verify the function is callable and works correctly
204
+ expect(decue.registeredFunctions['myFunc']('direct')).to.equal('PROCESSED: direct');
205
+
206
+ // Cleanup
207
+ delete decue.registeredFunctions['myFunc'];
208
+ delete window.someNamespace;
209
+ });
210
+
211
+ it('can chain multiple registered functions', function () {
212
+ const name = uniqueName('test-chained-funcs');
213
+
214
+ // Register multiple functions
215
+ decue.registeredFunctions['addPrefix'] = function(value) {
216
+ return 'PREFIX_' + value;
217
+ };
218
+
219
+ decue.registeredFunctions['addSuffix'] = function(value) {
220
+ return value + '_SUFFIX';
221
+ };
222
+
223
+ // Create element using chained functions
224
+ createTemplate(name, '<div class="content">{text|addPrefix|addSuffix}</div>');
225
+
226
+ const el = createElement(name, {
227
+ 'data-test-element': '',
228
+ text: 'middle'
229
+ });
230
+
231
+ const root = getContentRoot(el, 'none');
232
+ const div = root.querySelector('.content');
233
+ expect(div).to.exist;
234
+ expect(div.textContent).to.equal('PREFIX_middle_SUFFIX');
235
+
236
+ // Cleanup
237
+ delete decue.registeredFunctions['addPrefix'];
238
+ delete decue.registeredFunctions['addSuffix'];
239
+ });
240
+
241
+ it('registered functions receive correct context', function () {
242
+ const name = uniqueName('test-func-context');
243
+
244
+ let receivedValue = null;
245
+
246
+ // Register a function that captures the value
247
+ decue.registeredFunctions['captureValue'] = function(value) {
248
+ receivedValue = value;
249
+ return value;
250
+ };
251
+
252
+ // Create element
253
+ createTemplate(name, '<div>{text|captureValue}</div>');
254
+
255
+ createElement(name, {
256
+ 'data-test-element': '',
257
+ text: 'test-value'
258
+ });
259
+
260
+ // Verify function received the correct value
261
+ expect(receivedValue).to.equal('test-value');
262
+
263
+ // Cleanup
264
+ delete decue.registeredFunctions['captureValue'];
265
+ });
266
+
267
+ it('registered functions can transform attribute values', function () {
268
+ const name = uniqueName('test-func-transform');
269
+
270
+ // Register a transformation function
271
+ decue.registeredFunctions['double'] = function(value) {
272
+ return parseInt(value) * 2;
273
+ };
274
+
275
+ // Create element
276
+ createTemplate(name, '<div class="result">{number|double}</div>');
277
+
278
+ const el = createElement(name, {
279
+ 'data-test-element': '',
280
+ number: '5'
281
+ });
282
+
283
+ const root = getContentRoot(el, 'none');
284
+ const div = root.querySelector('.result');
285
+ expect(div).to.exist;
286
+ expect(div.textContent).to.equal('10');
287
+
288
+ // Update and verify transformation still works
289
+ el.setAttribute('number', '7');
290
+ expect(div.textContent).to.equal('14');
291
+
292
+ // Cleanup
293
+ delete decue.registeredFunctions['double'];
294
+ });
295
+
296
+ it('can mix registered functions with element properties', function () {
297
+ const name = uniqueName('test-mixed-pipes');
298
+
299
+ // Register a function
300
+ decue.registeredFunctions['wrap'] = function(value) {
301
+ return `(${value})`;
302
+ };
303
+
304
+ // Create element that uses both registered function and element property
305
+ createTemplate(name, '<div class="tag">{.tagName|wrap}</div>');
306
+
307
+ const el = createElement(name, {
308
+ 'data-test-element': ''
309
+ });
310
+
311
+ const root = getContentRoot(el, 'none');
312
+ const div = root.querySelector('.tag');
313
+ expect(div).to.exist;
314
+ expect(div.textContent).to.equal(`(${name.toUpperCase()})`);
315
+
316
+ // Cleanup
317
+ delete decue.registeredFunctions['wrap'];
318
+ });
319
+ });
320
+
321
+ describe('processTemplate API', function() {
322
+ it('can process templates manually', function () {
323
+ const name = uniqueName('test-manual-process');
324
+
325
+ // Create template manually
326
+ const template = document.createElement('template');
327
+ template.setAttribute('decue', name);
328
+ template.innerHTML = '<div>Manual template</div>';
329
+
330
+ // Process it
331
+ decue.processTemplate(template);
332
+
333
+ // Verify element is defined
334
+ expect(customElements.get(name)).to.exist;
335
+
336
+ // Create element from it
337
+ const el = createElement(name, {
338
+ 'data-test-element': ''
339
+ });
340
+
341
+ const root = getContentRoot(el, 'none');
342
+ const div = root.querySelector('div');
343
+ expect(div).to.exist;
344
+ expect(div.textContent).to.equal('Manual template');
345
+
346
+ template.remove();
347
+ });
348
+
349
+ it('processTemplate skips if element is already defined', function () {
350
+ const name = uniqueName('test-skip-reprocess');
351
+
352
+ // Define element first
353
+ createTemplate(name, '<div>First</div>');
354
+ expect(customElements.get(name)).to.exist;
355
+
356
+ // Try to process again with different content
357
+ const template2 = document.createElement('template');
358
+ template2.setAttribute('decue', name);
359
+ template2.innerHTML = '<div>Second</div>';
360
+
361
+ // Process should skip
362
+ decue.processTemplate(template2);
363
+
364
+ // Should still use first definition
365
+ const el = createElement(name, {
366
+ 'data-test-element': ''
367
+ });
368
+
369
+ const root = getContentRoot(el, 'none');
370
+ const div = root.querySelector('div');
371
+ expect(div.textContent).to.equal('First');
372
+
373
+ template2.remove();
374
+ });
375
+
376
+ it('processTemplate handles form-associated attribute', function () {
377
+ const name = uniqueName('test-process-form');
378
+
379
+ // Create template with form-associated attribute
380
+ const template = document.createElement('template');
381
+ template.setAttribute('decue', name);
382
+ template.setAttribute('form-associated', '');
383
+ template.innerHTML = '<div>Form element</div>';
384
+
385
+ // Process it
386
+ decue.processTemplate(template);
387
+
388
+ // Verify element is defined as form-associated
389
+ const elementClass = customElements.get(name);
390
+ expect(elementClass).to.exist;
391
+ expect(elementClass.formAssociated).to.be.true;
392
+
393
+ template.remove();
394
+ });
395
+ });
396
+
397
+ describe('integration scenarios', function() {
398
+ it('predefined element with template and observed attributes works together', function () {
399
+ const name = uniqueName('test-integration');
400
+
401
+ // Define element with observed attributes first
402
+ decue.defineElement(false, name, undefined, false, ['greeting', 'name']);
403
+
404
+ // Create template
405
+ const template = document.createElement('template');
406
+ template.setAttribute('decue', name);
407
+ template.innerHTML = '<div>{greeting} {name}!</div>';
408
+ document.body.insertBefore(template, document.body.firstChild);
409
+
410
+ // Create element
411
+ const el = createElement(name, {
412
+ 'data-test-element': '',
413
+ greeting: 'Hello',
414
+ name: 'World'
415
+ });
416
+
417
+ const root = getContentRoot(el, 'none');
418
+ const div = root.querySelector('div');
419
+ expect(div).to.exist;
420
+ expect(div.textContent).to.equal('Hello World!');
421
+
422
+ // Verify attributes are observed
423
+ el.setAttribute('greeting', 'Hi');
424
+ expect(div.textContent).to.equal('Hi World!');
425
+ });
426
+
427
+ it('form-associated element with registered functions works', function () {
428
+ const name = uniqueName('test-form-with-func');
429
+
430
+ // Register function
431
+ decue.registeredFunctions['currency'] = function(value) {
432
+ return `$${parseFloat(value).toFixed(2)}`;
433
+ };
434
+
435
+ // Define form-associated element
436
+ decue.defineElement(false, name, undefined, true, []);
437
+
438
+ // Create template
439
+ const template = document.createElement('template');
440
+ template.setAttribute('decue', name);
441
+ template.innerHTML = '<div>Amount: {value|currency}</div>';
442
+ document.body.insertBefore(template, document.body.firstChild);
443
+
444
+ // Create element
445
+ const el = createElement(name, {
446
+ 'data-test-element': '',
447
+ value: '42.5'
448
+ });
449
+
450
+ // Verify it's form-associated
451
+ expect(el.internals).to.exist;
452
+ expect(el.value).to.equal('42.5');
453
+
454
+ // Verify function works
455
+ const root = getContentRoot(el, 'none');
456
+ const div = root.querySelector('div');
457
+ expect(div).to.exist;
458
+ expect(div.textContent).to.equal('Amount: $42.50');
459
+
460
+ // Cleanup
461
+ delete decue.registeredFunctions['currency'];
462
+ });
463
+ });
464
+ });