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.
- package/README.md +15 -1
- package/dist/decue.js +333 -191
- package/dist/decue.min.js +1 -1
- package/eslint.config.js +53 -0
- package/examples.html +82 -12
- package/external.html +1 -0
- package/npm.sh +1 -1
- package/package.json +24 -11
- package/src/decue.js +333 -191
- package/test/debug.test.js +201 -0
- package/test/decue.test.js +78 -0
- package/test/errors.test.js +212 -0
- package/test/eventHandlers.test.js +95 -0
- package/test/formAssociated.test.js +477 -0
- package/test/init.test.js +43 -0
- package/test/lifecycle.test.js +110 -0
- package/test/memory.test.js +283 -0
- package/test/piped.test.js +152 -0
- package/test/placeholders.test.js +396 -0
- package/test/predefined.test.js +131 -0
- package/test/scriptAttributes.test.js +464 -0
- package/test/slots.test.js +293 -0
- package/test/test-helpers.js +36 -0
- package/tsconfig.json +1 -1
- package/web-test-runner.config.mjs +30 -0
- package/serve.sh +0 -4
- package/test/decue-tests.js +0 -84
- package/test/index.html +0 -65
- package/test/util/util.js +0 -17
|
@@ -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
|
+
});
|