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,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
|
+
});
|