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,201 @@
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('debug', () => {
18
+
19
+ ['none', 'open', 'closed'].forEach(shadow => {
20
+ describe("shadowmode: " + shadow, function() {
21
+ it('debug mode adds data attributes for observed attributes', function () {
22
+ const name = uniqueName('test-debug-observed-attrs');
23
+
24
+ // Create a template first
25
+ const template = document.createElement('template');
26
+ template.setAttribute('decue', name);
27
+ template.setAttribute('debug', '');
28
+ template.setAttribute('observed-attributes', 'value,data-test');
29
+ if (shadow !== 'none') {
30
+ template.setAttribute('shadow', shadow);
31
+ }
32
+ template.innerHTML = '<div>Test</div>';
33
+ document.head.appendChild(template);
34
+
35
+ // Process template
36
+ decue.processTemplate(template);
37
+
38
+ const el = document.createElement(name);
39
+ el.setAttribute('data-test-element', '');
40
+ if (shadow !== 'none') {
41
+ el.setAttribute('shadow', shadow);
42
+ }
43
+
44
+ document.body.appendChild(el);
45
+
46
+ // Check if debug attribute was added
47
+ expect(el).to.exist;
48
+ if (el.hasAttribute('data-decue-observed-attributes')) {
49
+ expect(el.getAttribute('data-decue-observed-attributes')).to.include('value');
50
+ expect(el.getAttribute('data-decue-observed-attributes')).to.include('data-test');
51
+ }
52
+ });
53
+
54
+ it('debug mode adds data attributes for mutation observed attributes', function () {
55
+ const name = uniqueName('test-debug-mutation-attrs');
56
+
57
+ const template = document.createElement('template');
58
+ template.setAttribute('decue', name);
59
+ template.setAttribute('debug', '');
60
+ if (shadow !== 'none') {
61
+ template.setAttribute('shadow', shadow);
62
+ }
63
+ template.innerHTML = '<div class="content">{text}</div>';
64
+ document.head.appendChild(template);
65
+
66
+ decue.processTemplate(template);
67
+
68
+ const el = createElement(name, {
69
+ 'data-test-element': '',
70
+ text: 'test value',
71
+ ...(shadow !== 'none' ? { shadow } : {})
72
+ });
73
+
74
+ // Check if debug attribute was added
75
+ expect(el).to.exist;
76
+
77
+ // Should have mutation observer attributes marked
78
+ if (el.hasAttribute('data-decue-mutation-observed-attributes')) {
79
+ const attrs = el.getAttribute('data-decue-mutation-observed-attributes');
80
+ expect(attrs).to.be.a('string');
81
+ }
82
+ });
83
+
84
+ it('debug mode logs to console when enabled', function () {
85
+ const name = uniqueName('test-debug-console');
86
+ const originalLog = console.log;
87
+ let logCalled = false;
88
+ let logMessages = [];
89
+
90
+ // Mock console.log BEFORE creating the element
91
+ console.log = function(...args) {
92
+ logCalled = true;
93
+ logMessages.push(args);
94
+ // Don't call originalLog to keep test output clean
95
+ };
96
+
97
+ try {
98
+ // Create template
99
+ const template = document.createElement('template');
100
+ template.setAttribute('decue', name);
101
+ if (shadow !== 'none') {
102
+ template.setAttribute('shadow', shadow);
103
+ }
104
+ template.innerHTML = '<div>{testattr}</div>';
105
+ document.head.appendChild(template);
106
+
107
+ // Use defineElement directly with debug=true instead of processTemplate
108
+ // because processTemplate uses the module-level debug variable
109
+ decue.defineElement(
110
+ true, // debug enabled
111
+ name,
112
+ template,
113
+ false,
114
+ ['testattr']
115
+ );
116
+
117
+ const el = document.createElement(name);
118
+ el.setAttribute('data-test-element', '');
119
+ el.setAttribute('testattr', 'value');
120
+ if (shadow !== 'none') {
121
+ el.setAttribute('shadow', shadow);
122
+ }
123
+
124
+ document.body.appendChild(el);
125
+
126
+ // Debug mode should have logged something when element connects
127
+ expect(logCalled).to.be.true;
128
+ expect(logMessages.length).to.be.greaterThan(0);
129
+
130
+ // Verify the first argument of at least one log call is the element name
131
+ const hasElementName = logMessages.some(args => args[0] === name);
132
+ expect(hasElementName).to.be.true;
133
+
134
+ // Verify "Finalizing..." message was logged
135
+ const hasFinalizingMessage = logMessages.some(args =>
136
+ args.length > 1 && args[1] === 'Finalizing...'
137
+ );
138
+ expect(hasFinalizingMessage).to.be.true;
139
+ } finally {
140
+ // Restore console.log
141
+ console.log = originalLog;
142
+ }
143
+ });
144
+
145
+ it('non-debug mode does not add debug attributes', function () {
146
+ const name = uniqueName('test-no-debug');
147
+
148
+ const template = document.createElement('template');
149
+ template.setAttribute('decue', name);
150
+ template.setAttribute('observed-attributes', 'value,data-test');
151
+ if (shadow !== 'none') {
152
+ template.setAttribute('shadow', shadow);
153
+ }
154
+ template.innerHTML = '<div>Test</div>';
155
+ document.head.appendChild(template);
156
+
157
+ decue.processTemplate(template);
158
+
159
+ const el = document.createElement(name);
160
+ el.setAttribute('data-test-element', '');
161
+ if (shadow !== 'none') {
162
+ el.setAttribute('shadow', shadow);
163
+ }
164
+
165
+ document.body.appendChild(el);
166
+
167
+ // Should not have debug attributes
168
+ expect(el.hasAttribute('data-decue-observed-attributes')).to.be.false;
169
+ expect(el.hasAttribute('data-decue-mutation-observed-attributes')).to.be.false;
170
+ });
171
+
172
+ it('debug mode works with template elements', function () {
173
+ const name = uniqueName('test-debug-template');
174
+
175
+ const template = document.createElement('template');
176
+ template.setAttribute('decue', name);
177
+ template.setAttribute('debug', '');
178
+ if (shadow !== 'none') {
179
+ template.setAttribute('shadow', shadow);
180
+ }
181
+ template.innerHTML = '<div class="content">{value}</div>';
182
+ document.head.appendChild(template);
183
+
184
+ // Process the template (simulate DeCuE's automatic processing)
185
+ decue.processTemplate(template);
186
+
187
+ const el = document.createElement(name);
188
+ el.setAttribute('data-test-element', '');
189
+ el.setAttribute('value', 'test');
190
+ if (shadow !== 'none') {
191
+ el.setAttribute('shadow', shadow);
192
+ }
193
+
194
+ document.body.appendChild(el);
195
+
196
+ expect(el).to.exist;
197
+ expect(el.isConnected).to.be.true;
198
+ });
199
+ });
200
+ });
201
+ });
@@ -0,0 +1,78 @@
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('decue', () => {
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('shadowroot is present or not as expected', function () {
26
+ const name = uniqueName('test-shadow-element');
27
+ createTemplate(name, '<div>Shadow test</div>', { shadow: shadow !== 'none' ? shadow : undefined });
28
+
29
+ const el = createElement(name, {
30
+ 'data-test-element': '',
31
+ ...(shadow !== 'none' ? { shadow } : {})
32
+ });
33
+
34
+ if (shadow === 'none') {
35
+ // In 'none' mode, content is placed directly in the element (no shadow root)
36
+ expect(el.shadowRoot).to.be.null;
37
+ expect(el.querySelector('div')).to.exist;
38
+ expect(el.querySelector('div').textContent).to.equal('Shadow test');
39
+ } else if (shadow === 'open') {
40
+ // In 'open' mode, shadowRoot is accessible
41
+ expect(el.shadowRoot).to.exist;
42
+ expect(el.shadowRoot.querySelector('div')).to.exist;
43
+ expect(el.shadowRoot.querySelector('div').textContent).to.equal('Shadow test');
44
+ } else if (shadow === 'closed') {
45
+ // In 'closed' mode, shadowRoot is not accessible from outside
46
+ // But the content should still be rendered
47
+ expect(el.shadowRoot).to.be.null;
48
+ // We can't directly check closed shadow root content from outside
49
+ }
50
+ });
51
+
52
+ it('element with no attributes works', function () {
53
+ const name = uniqueName('test-no-attr-element');
54
+ createTemplate(name, '<div class="content">Static content</div>', { shadow: shadow !== 'none' ? shadow : undefined });
55
+
56
+ const el = createElement(name, {
57
+ 'data-test-element': '',
58
+ ...(shadow !== 'none' ? { shadow } : {})
59
+ });
60
+
61
+ // Verify element was created successfully
62
+ expect(el).to.exist;
63
+ expect(el.tagName.toLowerCase()).to.equal(name);
64
+
65
+ const root = getContentRoot(el, shadow);
66
+ if (shadow !== 'closed') {
67
+ expect(root.querySelector('.content')).to.exist;
68
+ expect(root.querySelector('.content').textContent).to.equal('Static content');
69
+ } else {
70
+ // For closed shadow DOM, verify shadowRoot is not accessible
71
+ expect(el.shadowRoot).to.be.null;
72
+ // But the element should still be properly initialized
73
+ expect(el.isConnected).to.be.true;
74
+ }
75
+ });
76
+ });
77
+ });
78
+ });
@@ -0,0 +1,212 @@
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('errors', () => {
18
+
19
+ ['none', 'open', 'closed'].forEach(shadow => {
20
+ describe("shadowmode: " + shadow, function() {
21
+ it('element placeholder can reference element property', function () {
22
+ const name = uniqueName('test-element-property');
23
+ createTemplate(name, '<div class="content">Tag: {.tagName}</div>', { shadow: shadow !== 'none' ? shadow : undefined });
24
+
25
+ const el = createElement(name, {
26
+ 'data-test-element': '',
27
+ ...(shadow !== 'none' ? { shadow } : {})
28
+ });
29
+
30
+ expect(el).to.exist;
31
+
32
+ const root = getContentRoot(el, shadow);
33
+ if (shadow !== 'closed') {
34
+ const div = root.querySelector('.content');
35
+ expect(div).to.exist;
36
+ expect(div.textContent).to.equal(`Tag: ${name.toUpperCase()}`);
37
+ }
38
+ });
39
+
40
+ it('formAssociated element has correct class property', function () {
41
+ const name = uniqueName('test-form-class');
42
+
43
+ // Define element as formAssociated
44
+ decue.defineElement(false, name, null, true, []);
45
+
46
+ const elementClass = customElements.get(name);
47
+ expect(elementClass).to.exist;
48
+ expect(elementClass.formAssociated).to.be.true;
49
+ });
50
+
51
+ it('node cleanup works when element is removed and re-added', function () {
52
+ const name = uniqueName('test-node-cleanup');
53
+ createTemplate(name, '<div class="content">{text}</div>', { shadow: shadow !== 'none' ? shadow : undefined });
54
+
55
+ const el = createElement(name, {
56
+ 'data-test-element': '',
57
+ text: 'initial',
58
+ ...(shadow !== 'none' ? { shadow } : {})
59
+ });
60
+
61
+ const root = getContentRoot(el, shadow);
62
+
63
+ // Initial state
64
+ if (shadow !== 'closed') {
65
+ const div = root.querySelector('.content');
66
+ expect(div.textContent).to.equal('initial');
67
+ }
68
+
69
+ // Remove element from DOM
70
+ el.remove();
71
+
72
+ // Re-add to DOM
73
+ document.body.appendChild(el);
74
+
75
+ // Update attribute - should still work
76
+ el.setAttribute('text', 'updated');
77
+
78
+ if (shadow !== 'closed') {
79
+ const div = root.querySelector('.content');
80
+ expect(div.textContent).to.equal('updated');
81
+ }
82
+ });
83
+
84
+ it('node cleanup works with multiple disconnect/reconnect cycles', function () {
85
+ const name = uniqueName('test-multiple-cleanup');
86
+ createTemplate(name, '<div class="content">{value}</div>', { shadow: shadow !== 'none' ? shadow : undefined });
87
+
88
+ const el = createElement(name, {
89
+ 'data-test-element': '',
90
+ value: 'cycle-1',
91
+ ...(shadow !== 'none' ? { shadow } : {})
92
+ });
93
+
94
+ const root = getContentRoot(el, shadow);
95
+
96
+ // Cycle 1: disconnect and reconnect
97
+ el.remove();
98
+ document.body.appendChild(el);
99
+ el.setAttribute('value', 'cycle-2');
100
+
101
+ if (shadow !== 'closed') {
102
+ const div = root.querySelector('.content');
103
+ expect(div.textContent).to.equal('cycle-2');
104
+ }
105
+
106
+ // Cycle 2: disconnect and reconnect again
107
+ el.remove();
108
+ document.body.appendChild(el);
109
+ el.setAttribute('value', 'cycle-3');
110
+
111
+ if (shadow !== 'closed') {
112
+ const div = root.querySelector('.content');
113
+ expect(div.textContent).to.equal('cycle-3');
114
+ }
115
+ });
116
+
117
+ it('node cleanup works with multiple placeholders', function () {
118
+ const name = uniqueName('test-cleanup-multiple-placeholders');
119
+ createTemplate(name, '<div class="first">{attr1}</div><div class="second">{attr2}</div>', { shadow: shadow !== 'none' ? shadow : undefined });
120
+
121
+ const el = createElement(name, {
122
+ 'data-test-element': '',
123
+ attr1: 'first',
124
+ attr2: 'second',
125
+ ...(shadow !== 'none' ? { shadow } : {})
126
+ });
127
+
128
+ const root = getContentRoot(el, shadow);
129
+
130
+ // Remove and re-add
131
+ el.remove();
132
+ document.body.appendChild(el);
133
+
134
+ // Update both attributes
135
+ el.setAttribute('attr1', 'updated-first');
136
+ el.setAttribute('attr2', 'updated-second');
137
+
138
+ if (shadow !== 'closed') {
139
+ const first = root.querySelector('.first');
140
+ const second = root.querySelector('.second');
141
+ expect(first.textContent).to.equal('updated-first');
142
+ expect(second.textContent).to.equal('updated-second');
143
+ }
144
+ });
145
+ });
146
+ });
147
+
148
+ describe('special cases', function() {
149
+ it('processes template that is already defined (skips re-definition)', function () {
150
+ const name = uniqueName('test-already-defined');
151
+
152
+ // Define first
153
+ createTemplate(name, '<div>First</div>');
154
+ expect(customElements.get(name)).to.exist;
155
+
156
+ // Try to process same element again
157
+ const template2 = document.createElement('template');
158
+ template2.setAttribute('decue', name);
159
+ template2.innerHTML = '<div>Second</div>';
160
+
161
+ // Process template should skip since already defined
162
+ decue.processTemplate(template2);
163
+
164
+ // Element should still use first definition
165
+ const el = createElement(name, {
166
+ 'data-test-element': ''
167
+ });
168
+
169
+ const root = getContentRoot(el, 'none');
170
+ const div = root.querySelector('div');
171
+ expect(div.textContent).to.equal('First');
172
+
173
+ template2.remove();
174
+ });
175
+
176
+ it('fires events even when CustomEvent is unavailable', function (done) {
177
+ const name = uniqueName('test-event-fallback');
178
+ createTemplate(name, '<div>Content</div>');
179
+
180
+ // Save original CustomEvent
181
+ const originalCustomEvent = window.CustomEvent;
182
+
183
+ try {
184
+ // Temporarily remove CustomEvent to test fallback
185
+ // @ts-ignore
186
+ delete window.CustomEvent;
187
+
188
+ const el = document.createElement(name);
189
+ el.setAttribute('data-test-element', '');
190
+
191
+ let eventFired = false;
192
+ el.addEventListener('connect', () => {
193
+ eventFired = true;
194
+ });
195
+
196
+ document.body.appendChild(el);
197
+
198
+ setTimeout(() => {
199
+ expect(eventFired).to.be.true;
200
+
201
+ // Restore CustomEvent
202
+ window.CustomEvent = originalCustomEvent;
203
+ done();
204
+ }, 50);
205
+ } catch (e) {
206
+ // Restore in case of error
207
+ window.CustomEvent = originalCustomEvent;
208
+ throw e;
209
+ }
210
+ });
211
+ });
212
+ });
@@ -0,0 +1,95 @@
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('eventHandlers', () => {
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('decue-on can catch all of the known events', function (done) {
26
+ const name = uniqueName('test-decue-on-known');
27
+ createTemplate(name, '<div data-value="{testattr}">Content</div>', { shadow: shadow !== 'none' ? shadow : undefined });
28
+
29
+ let eventsCaught = [];
30
+
31
+ // Register global handler
32
+ window.testEventHandler = function(e) {
33
+ eventsCaught.push(e.type);
34
+ };
35
+
36
+ const el = document.createElement(name);
37
+ el.setAttribute('data-test-element', '');
38
+ el.setAttribute('decue-on:connect', 'testEventHandler');
39
+ el.setAttribute('decue-on:disconnect', 'testEventHandler');
40
+ el.setAttribute('decue-on:attributechange', 'testEventHandler');
41
+ el.setAttribute('testattr', 'initial');
42
+ if (shadow !== 'none') {
43
+ el.setAttribute('shadow', shadow);
44
+ }
45
+
46
+ document.body.appendChild(el);
47
+
48
+ setTimeout(() => {
49
+ el.setAttribute('testattr', 'value');
50
+
51
+ setTimeout(() => {
52
+ el.remove();
53
+
54
+ setTimeout(() => {
55
+ expect(eventsCaught).to.include('connect');
56
+ expect(eventsCaught).to.include('disconnect');
57
+ expect(eventsCaught).to.include('attributechange');
58
+ delete window.testEventHandler;
59
+ done();
60
+ }, 50);
61
+ }, 50);
62
+ }, 50);
63
+ });
64
+
65
+ it('decue-on can catch any unknown event', function (done) {
66
+ const name = uniqueName('test-decue-on-custom');
67
+ createTemplate(name, '<div>Content</div>', { shadow: shadow !== 'none' ? shadow : undefined });
68
+
69
+ let customEventCaught = false;
70
+
71
+ // Register global handler
72
+ window.customEventHandler = function(e) {
73
+ if (e.type === 'customevent') {
74
+ customEventCaught = true;
75
+ }
76
+ };
77
+
78
+ const el = createElement(name, {
79
+ 'data-test-element': '',
80
+ 'decue-on:customevent': 'customEventHandler',
81
+ ...(shadow !== 'none' ? { shadow } : {})
82
+ });
83
+
84
+ // Dispatch a custom event
85
+ el.dispatchEvent(new CustomEvent('customevent', { detail: { test: true } }));
86
+
87
+ setTimeout(() => {
88
+ expect(customEventCaught).to.be.true;
89
+ delete window.customEventHandler;
90
+ done();
91
+ }, 50);
92
+ });
93
+ });
94
+ });
95
+ });