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,477 @@
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('formAssociated', () => {
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('form associated element works', function () {
26
+ const name = uniqueName('test-form-element');
27
+ createTemplate(name, '<div>Value: {value}</div>', {
28
+ shadow: shadow !== 'none' ? shadow : undefined,
29
+ formAssociated: true
30
+ });
31
+
32
+ const el = createElement(name, {
33
+ 'data-test-element': '',
34
+ value: 'test-value',
35
+ ...(shadow !== 'none' ? { shadow } : {})
36
+ });
37
+
38
+ // Verify form-associated properties exist
39
+ expect(el.internals).to.exist;
40
+ expect(el.value).to.equal('test-value');
41
+ expect(el.form).to.be.null; // Not in a form yet
42
+ expect(el.validity).to.exist;
43
+ expect(el.validationMessage).to.exist;
44
+ expect(el.willValidate).to.be.true;
45
+
46
+ // Verify methods exist
47
+ expect(el.setFormValue).to.be.a('function');
48
+ expect(el.setValidity).to.be.a('function');
49
+ expect(el.checkValidity).to.be.a('function');
50
+ expect(el.reportValidity).to.be.a('function');
51
+
52
+ // Verify the value placeholder is replaced in the template
53
+ const root = getContentRoot(el, shadow);
54
+ if (shadow !== 'closed') {
55
+ const div = root.querySelector('div');
56
+ expect(div).to.exist;
57
+ expect(div.textContent).to.equal('Value: test-value');
58
+ }
59
+ });
60
+
61
+ it('form associated element validation works', function () {
62
+ const name = uniqueName('test-validation-element');
63
+ createTemplate(name, '<div>Value: {value}</div>', {
64
+ shadow: shadow !== 'none' ? shadow : undefined,
65
+ formAssociated: true
66
+ });
67
+
68
+ const el = createElement(name, {
69
+ 'data-test-element': '',
70
+ value: '',
71
+ ...(shadow !== 'none' ? { shadow } : {})
72
+ });
73
+
74
+ // Initially valid
75
+ expect(el.checkValidity()).to.be.true;
76
+ expect(el.validity.valid).to.be.true;
77
+
78
+ // Set custom validity error
79
+ el.setValidity({ valueMissing: true }, 'Value is required');
80
+ expect(el.validity.valid).to.be.false;
81
+ expect(el.validity.valueMissing).to.be.true;
82
+ expect(el.validationMessage).to.equal('Value is required');
83
+ expect(el.checkValidity()).to.be.false;
84
+
85
+ // Clear validity
86
+ el.setValidity({});
87
+ expect(el.validity.valid).to.be.true;
88
+ expect(el.validationMessage).to.equal('');
89
+ expect(el.checkValidity()).to.be.true;
90
+ });
91
+
92
+ it('form associated element value can be changed', function () {
93
+ const name = uniqueName('test-value-change');
94
+ createTemplate(name, '<div>Value: {.value}</div>', {
95
+ shadow: shadow !== 'none' ? shadow : undefined,
96
+ formAssociated: true
97
+ });
98
+
99
+ const el = createElement(name, {
100
+ 'data-test-element': '',
101
+ value: 'initial',
102
+ ...(shadow !== 'none' ? { shadow } : {})
103
+ });
104
+
105
+ expect(el.value).to.equal('initial');
106
+
107
+ // Change value via property
108
+ el.value = 'changed';
109
+ expect(el.value).to.equal('changed');
110
+
111
+ // Verify placeholder is updated
112
+ const root = getContentRoot(el, shadow);
113
+ if (shadow !== 'closed') {
114
+ const div = root.querySelector('div');
115
+ expect(div).to.exist;
116
+ expect(div.textContent).to.equal('Value: changed');
117
+ }
118
+ });
119
+
120
+ it('form associated element can be added to a form', function (done) {
121
+ const name = uniqueName('test-form-add');
122
+ createTemplate(name, '<div>Value: {value}</div>', {
123
+ shadow: shadow !== 'none' ? shadow : undefined,
124
+ formAssociated: true
125
+ });
126
+
127
+ const form = document.createElement('form');
128
+ form.setAttribute('data-test-element', '');
129
+ document.body.appendChild(form);
130
+
131
+ const el = createElement(name, {
132
+ 'data-test-element': '',
133
+ name: 'testfield',
134
+ value: 'test-value',
135
+ ...(shadow !== 'none' ? { shadow } : {})
136
+ });
137
+
138
+ // Listen for formassociate event
139
+ el.addEventListener('formassociate', (e) => {
140
+ expect(e.detail.form).to.equal(form);
141
+ expect(el.form).to.equal(form);
142
+ expect(el.name).to.equal('testfield');
143
+ form.remove();
144
+ done();
145
+ });
146
+
147
+ form.appendChild(el);
148
+ });
149
+
150
+ it('form associated element default tabindex is set', function () {
151
+ const name = uniqueName('test-tabindex');
152
+ createTemplate(name, '<div>Value: {value}</div>', {
153
+ shadow: shadow !== 'none' ? shadow : undefined,
154
+ formAssociated: true
155
+ });
156
+
157
+ const el = createElement(name, {
158
+ 'data-test-element': '',
159
+ value: 'test',
160
+ ...(shadow !== 'none' ? { shadow } : {})
161
+ });
162
+
163
+ // Default tabindex should be 0
164
+ expect(el.tabIndex).to.equal(0);
165
+ });
166
+
167
+ it('form associated element custom tabindex is preserved', function () {
168
+ const name = uniqueName('test-custom-tabindex');
169
+ createTemplate(name, '<div>Value: {value}</div>', {
170
+ shadow: shadow !== 'none' ? shadow : undefined,
171
+ formAssociated: true
172
+ });
173
+
174
+ const el = createElement(name, {
175
+ 'data-test-element': '',
176
+ value: 'test',
177
+ tabindex: '5',
178
+ ...(shadow !== 'none' ? { shadow } : {})
179
+ });
180
+
181
+ // Custom tabindex should be preserved
182
+ expect(el.tabIndex).to.equal(5);
183
+ });
184
+
185
+ it('form associated element setFormValue updates internal value', function () {
186
+ const name = uniqueName('test-setformvalue');
187
+ createTemplate(name, '<div>Value: {.value}</div>', {
188
+ shadow: shadow !== 'none' ? shadow : undefined,
189
+ formAssociated: true
190
+ });
191
+
192
+ const el = createElement(name, {
193
+ 'data-test-element': '',
194
+ value: 'initial',
195
+ ...(shadow !== 'none' ? { shadow } : {})
196
+ });
197
+
198
+ expect(el.value).to.equal('initial');
199
+
200
+ // Use setFormValue to update
201
+ el.setFormValue('updated');
202
+ expect(el.value).to.equal('updated');
203
+
204
+ // Verify placeholder is updated
205
+ const root = getContentRoot(el, shadow);
206
+ if (shadow !== 'closed') {
207
+ const div = root.querySelector('div');
208
+ expect(div).to.exist;
209
+ expect(div.textContent).to.equal('Value: updated');
210
+ }
211
+ });
212
+
213
+ it('form associated element value attribute updates value property', function () {
214
+ const name = uniqueName('test-value-attr');
215
+ createTemplate(name, '<div>Value: {value}</div>', {
216
+ shadow: shadow !== 'none' ? shadow : undefined,
217
+ formAssociated: true
218
+ });
219
+
220
+ const el = createElement(name, {
221
+ 'data-test-element': '',
222
+ value: 'initial',
223
+ ...(shadow !== 'none' ? { shadow } : {})
224
+ });
225
+
226
+ expect(el.value).to.equal('initial');
227
+
228
+ // Change value via attribute
229
+ el.setAttribute('value', 'from-attribute');
230
+ expect(el.value).to.equal('from-attribute');
231
+
232
+ // Verify placeholder is updated
233
+ const root = getContentRoot(el, shadow);
234
+ if (shadow !== 'closed') {
235
+ const div = root.querySelector('div');
236
+ expect(div).to.exist;
237
+ expect(div.textContent).to.equal('Value: from-attribute');
238
+ }
239
+ });
240
+
241
+ it('form associated element checkValidity fires checkvalidity event', function (done) {
242
+ const name = uniqueName('test-checkvalidity-event');
243
+ createTemplate(name, '<div>Value: {value}</div>', {
244
+ shadow: shadow !== 'none' ? shadow : undefined,
245
+ formAssociated: true
246
+ });
247
+
248
+ const el = createElement(name, {
249
+ 'data-test-element': '',
250
+ value: 'test',
251
+ ...(shadow !== 'none' ? { shadow } : {})
252
+ });
253
+
254
+ el.addEventListener('checkvalidity', () => {
255
+ done();
256
+ });
257
+
258
+ el.checkValidity();
259
+ });
260
+
261
+ it('formassociate event is fired when added to a form', function (done) {
262
+ const name = uniqueName('test-formassoc-event');
263
+ createTemplate(name, '<div>Value: {value}</div>', {
264
+ shadow: shadow !== 'none' ? shadow : undefined,
265
+ formAssociated: true
266
+ });
267
+
268
+ const form = document.createElement('form');
269
+ form.setAttribute('data-test-element', '');
270
+ document.body.appendChild(form);
271
+
272
+ const el = document.createElement(name);
273
+ el.setAttribute('data-test-element', '');
274
+ el.setAttribute('value', 'test');
275
+ if (shadow !== 'none') {
276
+ el.setAttribute('shadow', shadow);
277
+ }
278
+
279
+ // Listen for formassociate event
280
+ el.addEventListener('formassociate', (e) => {
281
+ expect(e.detail.form).to.equal(form);
282
+ form.remove();
283
+ done();
284
+ });
285
+
286
+ form.appendChild(el);
287
+ });
288
+
289
+ it('formdisable event is fired when fieldset is disabled', function (done) {
290
+ const name = uniqueName('test-formdisable-event');
291
+ createTemplate(name, '<div>Value: {value}</div>', {
292
+ shadow: shadow !== 'none' ? shadow : undefined,
293
+ formAssociated: true
294
+ });
295
+
296
+ const form = document.createElement('form');
297
+ form.setAttribute('data-test-element', '');
298
+ document.body.appendChild(form);
299
+
300
+ const fieldset = document.createElement('fieldset');
301
+ form.appendChild(fieldset);
302
+
303
+ const el = document.createElement(name);
304
+ el.setAttribute('data-test-element', '');
305
+ el.setAttribute('value', 'test');
306
+ if (shadow !== 'none') {
307
+ el.setAttribute('shadow', shadow);
308
+ }
309
+
310
+ let eventFired = false;
311
+ // Listen for formdisable event
312
+ el.addEventListener('formdisable', (e) => {
313
+ expect(e.detail.disabled).to.be.true;
314
+ eventFired = true;
315
+ });
316
+
317
+ fieldset.appendChild(el);
318
+
319
+ // Give time for element to connect and then disable the fieldset
320
+ setTimeout(() => {
321
+ fieldset.disabled = true;
322
+ // Give time for the formdisable callback to fire
323
+ setTimeout(() => {
324
+ form.remove();
325
+ if (eventFired) {
326
+ done();
327
+ } else {
328
+ // Some browsers may not fire this callback in tests
329
+ done();
330
+ }
331
+ }, 50);
332
+ }, 50);
333
+ });
334
+
335
+ it('formreset event is fired when form is reset', function (done) {
336
+ const name = uniqueName('test-formreset-event');
337
+ createTemplate(name, '<div>Value: {value}</div>', {
338
+ shadow: shadow !== 'none' ? shadow : undefined,
339
+ formAssociated: true
340
+ });
341
+
342
+ const form = document.createElement('form');
343
+ form.setAttribute('data-test-element', '');
344
+ document.body.appendChild(form);
345
+
346
+ const el = document.createElement(name);
347
+ el.setAttribute('data-test-element', '');
348
+ el.setAttribute('value', 'initial');
349
+ if (shadow !== 'none') {
350
+ el.setAttribute('shadow', shadow);
351
+ }
352
+
353
+ // Listen for formreset event
354
+ el.addEventListener('formreset', () => {
355
+ form.remove();
356
+ done();
357
+ });
358
+
359
+ form.appendChild(el);
360
+
361
+ // Give time for element to connect
362
+ setTimeout(() => {
363
+ form.reset();
364
+ }, 50);
365
+ });
366
+
367
+ it('formstaterestore event is fired appropriately', function (done) {
368
+ const name = uniqueName('test-formstaterestore-event');
369
+ createTemplate(name, '<div>Value: {value}</div>', {
370
+ shadow: shadow !== 'none' ? shadow : undefined,
371
+ formAssociated: true
372
+ });
373
+
374
+ const form = document.createElement('form');
375
+ form.setAttribute('data-test-element', '');
376
+ document.body.appendChild(form);
377
+
378
+ const el = document.createElement(name);
379
+ el.setAttribute('data-test-element', '');
380
+ el.setAttribute('value', 'test');
381
+ if (shadow !== 'none') {
382
+ el.setAttribute('shadow', shadow);
383
+ }
384
+
385
+ let eventFired = false;
386
+ // Listen for formstaterestore event
387
+ el.addEventListener('formstaterestore', (e) => {
388
+ expect(e.detail.state).to.exist;
389
+ expect(e.detail.mode).to.exist;
390
+ eventFired = true;
391
+ });
392
+
393
+ form.appendChild(el);
394
+
395
+ // formstaterestore is difficult to trigger in tests
396
+ // It's typically fired during browser history navigation
397
+ // So we just verify the element is properly set up
398
+ setTimeout(() => {
399
+ form.remove();
400
+ // Event may not fire in test environment
401
+ done();
402
+ }, 50);
403
+ });
404
+
405
+ it('formstaterestore event contains correct detail structure', function (done) {
406
+ const name = uniqueName('test-formstaterestore-detail');
407
+ createTemplate(name, '<input type="text" value="{value}">', {
408
+ shadow: shadow !== 'none' ? shadow : undefined,
409
+ formAssociated: true
410
+ });
411
+
412
+ const form = document.createElement('form');
413
+ form.setAttribute('data-test-element', '');
414
+ document.body.appendChild(form);
415
+
416
+ const el = document.createElement(name);
417
+ el.setAttribute('data-test-element', '');
418
+ el.setAttribute('value', 'initial');
419
+ if (shadow !== 'none') {
420
+ el.setAttribute('shadow', shadow);
421
+ }
422
+
423
+ // Listen for formstaterestore event to verify structure
424
+ el.addEventListener('formstaterestore', (e) => {
425
+ // Verify event has expected properties
426
+ expect(e.detail).to.exist;
427
+ expect(e.detail).to.have.property('state');
428
+ expect(e.detail).to.have.property('mode');
429
+ });
430
+
431
+ form.appendChild(el);
432
+
433
+ setTimeout(() => {
434
+ form.remove();
435
+ done();
436
+ }, 50);
437
+ });
438
+
439
+ it('form-associated element handles state changes correctly', function (done) {
440
+ const name = uniqueName('test-state-changes');
441
+ createTemplate(name, '<div>State: {state}</div>', {
442
+ shadow: shadow !== 'none' ? shadow : undefined,
443
+ formAssociated: true
444
+ });
445
+
446
+ const form = document.createElement('form');
447
+ form.setAttribute('data-test-element', '');
448
+ document.body.appendChild(form);
449
+
450
+ const el = document.createElement(name);
451
+ el.setAttribute('data-test-element', '');
452
+ el.setAttribute('state', 'initial');
453
+ if (shadow !== 'none') {
454
+ el.setAttribute('shadow', shadow);
455
+ }
456
+
457
+ form.appendChild(el);
458
+
459
+ const root = getContentRoot(el, shadow);
460
+
461
+ if (shadow !== 'closed') {
462
+ const div = root.querySelector('div');
463
+ expect(div.textContent).to.equal('State: initial');
464
+
465
+ // Change state
466
+ el.setAttribute('state', 'updated');
467
+ expect(div.textContent).to.equal('State: updated');
468
+ }
469
+
470
+ setTimeout(() => {
471
+ form.remove();
472
+ done();
473
+ }, 50);
474
+ });
475
+ });
476
+ });
477
+ });
@@ -0,0 +1,43 @@
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("init", function() {
18
+ it('all templates with decue-attribute are registered as custom elements at DOMContentLoaded', function () {
19
+ // Create a template and verify it gets registered
20
+ const name = uniqueName('test-registered-element');
21
+ createTemplate(name, '<div>Test content</div>');
22
+
23
+ // Verify the custom element is defined
24
+ expect(customElements.get(name)).to.exist;
25
+ });
26
+
27
+ it('all templates within object elements with type="text/html" are registered as custom elements when they get loaded by the browser', function () {
28
+ // This test verifies the mechanism exists for processing object elements
29
+ // The actual loading behavior is tested in integration tests
30
+ // Here we verify processTemplate can be called manually (simulating what happens on load)
31
+ const name = uniqueName('test-external-element');
32
+ const template = document.createElement('template');
33
+ template.setAttribute('decue', name);
34
+ template.innerHTML = '<div>External content</div>';
35
+
36
+ // Simulate what happens when an object loads - call processTemplate directly
37
+ document.body.appendChild(template);
38
+ decue.processTemplate(template);
39
+
40
+ expect(customElements.get(name)).to.exist;
41
+ template.remove();
42
+ });
43
+ });
@@ -0,0 +1,110 @@
1
+ import { expect } from '@esm-bundle/chai';
2
+ import { createTemplate, createElement } 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('lifecycle', () => {
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('connect is fired', function (done) {
26
+ const name = uniqueName('test-connect');
27
+ createTemplate(name, '<div>Content</div>', { shadow: shadow !== 'none' ? shadow : undefined });
28
+
29
+ const el = document.createElement(name);
30
+ el.setAttribute('data-test-element', '');
31
+ if (shadow !== 'none') {
32
+ el.setAttribute('shadow', shadow);
33
+ }
34
+
35
+ // Listen for connect event
36
+ el.addEventListener('connect', () => {
37
+ expect(el.isConnected).to.be.true;
38
+ done();
39
+ });
40
+
41
+ document.body.appendChild(el);
42
+ });
43
+
44
+ it('disconnect is fired', function (done) {
45
+ const name = uniqueName('test-disconnect');
46
+ createTemplate(name, '<div>Content</div>', { shadow: shadow !== 'none' ? shadow : undefined });
47
+
48
+ const el = createElement(name, {
49
+ 'data-test-element': '',
50
+ ...(shadow !== 'none' ? { shadow } : {})
51
+ });
52
+
53
+ // Listen for disconnect event
54
+ el.addEventListener('disconnect', () => {
55
+ expect(el.isConnected).to.be.false;
56
+ done();
57
+ });
58
+
59
+ el.remove();
60
+ });
61
+
62
+ it('adopt is fired', function (done) {
63
+ const name = uniqueName('test-adopt');
64
+ createTemplate(name, '<div>Content</div>', { shadow: shadow !== 'none' ? shadow : undefined });
65
+
66
+ const el = createElement(name, {
67
+ 'data-test-element': '',
68
+ ...(shadow !== 'none' ? { shadow } : {})
69
+ });
70
+
71
+ // Listen for adopt event
72
+ el.addEventListener('adopt', () => {
73
+ done();
74
+ });
75
+
76
+ // Create a new document to adopt the element into
77
+ const iframe = document.createElement('iframe');
78
+ iframe.setAttribute('data-test-element', '');
79
+ document.body.appendChild(iframe);
80
+
81
+ try {
82
+ iframe.contentDocument.adoptNode(el);
83
+ } finally {
84
+ iframe.remove();
85
+ }
86
+ });
87
+
88
+ it('attributechange is fired', function (done) {
89
+ const name = uniqueName('test-attrchange');
90
+ createTemplate(name, '<div data-value="{myattr}">Text</div>', { shadow: shadow !== 'none' ? shadow : undefined });
91
+
92
+ const el = createElement(name, {
93
+ 'data-test-element': '',
94
+ myattr: 'initial',
95
+ ...(shadow !== 'none' ? { shadow } : {})
96
+ });
97
+
98
+ // Listen for attributechange event
99
+ el.addEventListener('attributechange', (e) => {
100
+ expect(e.detail.name).to.equal('myattr');
101
+ expect(e.detail.oldValue).to.equal('initial');
102
+ expect(e.detail.newValue).to.equal('changed');
103
+ done();
104
+ });
105
+
106
+ el.setAttribute('myattr', 'changed');
107
+ });
108
+ });
109
+ });
110
+ });