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,293 @@
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('slots', () => {
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('named slot with default value', function () {
26
+ const name = uniqueName('test-named-slot-default');
27
+ createTemplate(name, '<slot name="content"><div class="default">Default slot content</div></slot>', { shadow: shadow !== 'none' ? shadow : undefined });
28
+
29
+ const el = createElement(name, {
30
+ 'data-test-element': '',
31
+ ...(shadow !== 'none' ? { shadow } : {})
32
+ });
33
+
34
+ // Verify element was created
35
+ expect(el).to.exist;
36
+
37
+ const root = getContentRoot(el, shadow);
38
+ if (shadow !== 'closed') {
39
+ // When no slotted content is provided, default should be used
40
+ const defaultContent = root.querySelector('.default');
41
+ expect(defaultContent).to.exist;
42
+ expect(defaultContent.textContent).to.equal('Default slot content');
43
+ } else {
44
+ // For closed shadow, verify element exists and is connected
45
+ expect(el.shadowRoot).to.be.null;
46
+ expect(el.isConnected).to.be.true;
47
+ }
48
+ });
49
+
50
+ it('named slot with given value', function () {
51
+ const name = uniqueName('test-named-slot-given');
52
+ createTemplate(name, '<slot name="content"><div class="default">Default slot content</div></slot>', { shadow: shadow !== 'none' ? shadow : undefined });
53
+
54
+ const el = createElement(name, {
55
+ 'data-test-element': '',
56
+ ...(shadow !== 'none' ? { shadow } : {})
57
+ }, '<div slot="content" class="custom">Custom slot content</div>');
58
+
59
+ // Verify element was created
60
+ expect(el).to.exist;
61
+
62
+ if (shadow !== 'closed') {
63
+ if (shadow === 'none') {
64
+ // In light DOM (no shadow), slotted content replaces the slot in place
65
+ const root = getContentRoot(el, shadow);
66
+ const customContent = root.querySelector('.custom');
67
+ expect(customContent).to.exist;
68
+ expect(customContent.textContent).to.equal('Custom slot content');
69
+
70
+ // Default should not be present
71
+ const defaultContent = root.querySelector('.default');
72
+ expect(defaultContent).to.not.exist;
73
+ } else {
74
+ // In shadow DOM (open), slotted content stays in light DOM
75
+ // and is projected through the slot
76
+ const customContent = el.querySelector('.custom');
77
+ expect(customContent).to.exist;
78
+ expect(customContent.textContent).to.equal('Custom slot content');
79
+
80
+ // Shadow root should have the slot element
81
+ const slot = el.shadowRoot.querySelector('slot[name="content"]');
82
+ expect(slot).to.exist;
83
+
84
+ // Default should be in the shadow root
85
+ const defaultContent = el.shadowRoot.querySelector('.default');
86
+ expect(defaultContent).to.exist;
87
+ }
88
+ } else {
89
+ // For closed shadow, verify element exists and is connected
90
+ expect(el.shadowRoot).to.be.null;
91
+ expect(el.isConnected).to.be.true;
92
+
93
+ // Slotted content should be in the light DOM
94
+ const customContent = el.querySelector('.custom');
95
+ expect(customContent).to.exist;
96
+ }
97
+ });
98
+
99
+ it('default slot with default value', function () {
100
+ const name = uniqueName('test-default-slot-default');
101
+ createTemplate(name, '<slot><div class="default">Default content</div></slot>', { shadow: shadow !== 'none' ? shadow : undefined });
102
+
103
+ const el = createElement(name, {
104
+ 'data-test-element': '',
105
+ ...(shadow !== 'none' ? { shadow } : {})
106
+ });
107
+
108
+ // Verify element was created
109
+ expect(el).to.exist;
110
+
111
+ const root = getContentRoot(el, shadow);
112
+ if (shadow !== 'closed') {
113
+ // When no content is provided, default slot content should be used
114
+ const defaultContent = root.querySelector('.default');
115
+ expect(defaultContent).to.exist;
116
+ expect(defaultContent.textContent).to.equal('Default content');
117
+ } else {
118
+ // For closed shadow, verify element exists and is connected
119
+ expect(el.shadowRoot).to.be.null;
120
+ expect(el.isConnected).to.be.true;
121
+ }
122
+ });
123
+
124
+ it('default slot with given value', function () {
125
+ const name = uniqueName('test-default-slot-given');
126
+ createTemplate(name, '<slot><div class="default">Default content</div></slot>', { shadow: shadow !== 'none' ? shadow : undefined });
127
+
128
+ const el = createElement(name, {
129
+ 'data-test-element': '',
130
+ ...(shadow !== 'none' ? { shadow } : {})
131
+ }, '<div class="custom">Custom content</div>');
132
+
133
+ // Verify element was created
134
+ expect(el).to.exist;
135
+
136
+ if (shadow !== 'closed') {
137
+ if (shadow === 'none') {
138
+ // In light DOM (no shadow), slotted content replaces the slot in place
139
+ const root = getContentRoot(el, shadow);
140
+ const customContent = root.querySelector('.custom');
141
+ expect(customContent).to.exist;
142
+ expect(customContent.textContent).to.equal('Custom content');
143
+
144
+ // Default should not be present
145
+ const defaultContent = root.querySelector('.default');
146
+ expect(defaultContent).to.not.exist;
147
+ } else {
148
+ // In shadow DOM (open), slotted content stays in light DOM
149
+ const customContent = el.querySelector('.custom');
150
+ expect(customContent).to.exist;
151
+ expect(customContent.textContent).to.equal('Custom content');
152
+
153
+ // Shadow root should have the slot element
154
+ const slot = el.shadowRoot.querySelector('slot:not([name])');
155
+ expect(slot).to.exist;
156
+ }
157
+ } else {
158
+ // For closed shadow, verify element exists and is connected
159
+ expect(el.shadowRoot).to.be.null;
160
+ expect(el.isConnected).to.be.true;
161
+
162
+ // Slotted content should be in the light DOM
163
+ const customContent = el.querySelector('.custom');
164
+ expect(customContent).to.exist;
165
+ }
166
+ });
167
+
168
+ it('multiple named slots', function () {
169
+ const name = uniqueName('test-multiple-named-slots');
170
+ createTemplate(name,
171
+ '<slot name="header"><div class="default-header">Default Header</div></slot>' +
172
+ '<slot name="body"><div class="default-body">Default Body</div></slot>' +
173
+ '<slot name="footer"><div class="default-footer">Default Footer</div></slot>',
174
+ { shadow: shadow !== 'none' ? shadow : undefined }
175
+ );
176
+
177
+ const el = createElement(name, {
178
+ 'data-test-element': '',
179
+ ...(shadow !== 'none' ? { shadow } : {})
180
+ },
181
+ '<div slot="header" class="custom-header">Custom Header</div>' +
182
+ '<div slot="body" class="custom-body">Custom Body</div>'
183
+ );
184
+
185
+ // Verify element was created
186
+ expect(el).to.exist;
187
+
188
+ if (shadow !== 'closed') {
189
+ if (shadow === 'none') {
190
+ // In light DOM, slotted content replaces slots in place
191
+ const root = getContentRoot(el, shadow);
192
+
193
+ const customHeader = root.querySelector('.custom-header');
194
+ expect(customHeader).to.exist;
195
+ expect(customHeader.textContent).to.equal('Custom Header');
196
+
197
+ const customBody = root.querySelector('.custom-body');
198
+ expect(customBody).to.exist;
199
+ expect(customBody.textContent).to.equal('Custom Body');
200
+
201
+ // Footer should still use default (not provided)
202
+ const defaultFooter = root.querySelector('.default-footer');
203
+ expect(defaultFooter).to.exist;
204
+ expect(defaultFooter.textContent).to.equal('Default Footer');
205
+
206
+ // Defaults for header and body should not be present
207
+ expect(root.querySelector('.default-header')).to.not.exist;
208
+ expect(root.querySelector('.default-body')).to.not.exist;
209
+ } else {
210
+ // In shadow DOM (open), slotted content stays in light DOM
211
+ const customHeader = el.querySelector('.custom-header');
212
+ expect(customHeader).to.exist;
213
+ expect(customHeader.textContent).to.equal('Custom Header');
214
+
215
+ const customBody = el.querySelector('.custom-body');
216
+ expect(customBody).to.exist;
217
+ expect(customBody.textContent).to.equal('Custom Body');
218
+
219
+ // Shadow root should have slots
220
+ expect(el.shadowRoot.querySelector('slot[name="header"]')).to.exist;
221
+ expect(el.shadowRoot.querySelector('slot[name="body"]')).to.exist;
222
+ expect(el.shadowRoot.querySelector('slot[name="footer"]')).to.exist;
223
+
224
+ // Default for footer should be in shadow root
225
+ const defaultFooter = el.shadowRoot.querySelector('.default-footer');
226
+ expect(defaultFooter).to.exist;
227
+ }
228
+ } else {
229
+ // For closed shadow, verify element exists and is connected
230
+ expect(el.shadowRoot).to.be.null;
231
+ expect(el.isConnected).to.be.true;
232
+
233
+ // Slotted content should be in light DOM
234
+ expect(el.querySelector('.custom-header')).to.exist;
235
+ expect(el.querySelector('.custom-body')).to.exist;
236
+ }
237
+ });
238
+
239
+ it('multiple default slots', function () {
240
+ const name = uniqueName('test-multiple-default-slots');
241
+ // Multiple default slots is unusual but should work by distributing content
242
+ createTemplate(name,
243
+ '<div class="section1"><slot><span>Default 1</span></slot></div>' +
244
+ '<div class="section2"><slot><span>Default 2</span></slot></div>',
245
+ { shadow: shadow !== 'none' ? shadow : undefined }
246
+ );
247
+
248
+ const el = createElement(name, {
249
+ 'data-test-element': '',
250
+ ...(shadow !== 'none' ? { shadow } : {})
251
+ }, '<div class="custom">Custom content</div>');
252
+
253
+ // Verify element was created
254
+ expect(el).to.exist;
255
+
256
+ if (shadow !== 'closed') {
257
+ if (shadow === 'none') {
258
+ // In light DOM, first slot gets replaced with custom content
259
+ const root = getContentRoot(el, shadow);
260
+ const section1 = root.querySelector('.section1');
261
+ expect(section1).to.exist;
262
+ const customInSection1 = section1.querySelector('.custom');
263
+ expect(customInSection1).to.exist;
264
+
265
+ // Second slot gets its default
266
+ const section2 = root.querySelector('.section2');
267
+ expect(section2).to.exist;
268
+ const defaultInSection2 = section2.querySelector('span');
269
+ expect(defaultInSection2).to.exist;
270
+ expect(defaultInSection2.textContent).to.equal('Default 2');
271
+ } else {
272
+ // In shadow DOM (open), slotted content stays in light DOM
273
+ const customContent = el.querySelector('.custom');
274
+ expect(customContent).to.exist;
275
+ expect(customContent.textContent).to.equal('Custom content');
276
+
277
+ // Shadow root should have both sections with slots
278
+ expect(el.shadowRoot.querySelector('.section1')).to.exist;
279
+ expect(el.shadowRoot.querySelector('.section2')).to.exist;
280
+ }
281
+ } else {
282
+ // For closed shadow, verify element exists and is connected
283
+ expect(el.shadowRoot).to.be.null;
284
+ expect(el.isConnected).to.be.true;
285
+
286
+ // Slotted content should be in light DOM
287
+ const customContent = el.querySelector('.custom');
288
+ expect(customContent).to.exist;
289
+ }
290
+ });
291
+ });
292
+ });
293
+ });
@@ -0,0 +1,36 @@
1
+ // Helper to create a template and process it
2
+ export function createTemplate(name, content, options = {}) {
3
+ const template = document.createElement('template');
4
+ template.setAttribute('decue', name);
5
+ if (options.shadow) {
6
+ template.setAttribute('shadow', options.shadow);
7
+ }
8
+ if (options.formAssociated) {
9
+ template.setAttribute('form-associated', '');
10
+ }
11
+ template.innerHTML = content;
12
+ document.body.appendChild(template);
13
+ decue.processTemplate(template);
14
+ return template;
15
+ }
16
+
17
+ // Helper to create and append a custom element
18
+ export function createElement(tagName, attributes = {}, innerHTML = '') {
19
+ const el = document.createElement(tagName);
20
+ for (const [key, value] of Object.entries(attributes)) {
21
+ el.setAttribute(key, value);
22
+ }
23
+ if (innerHTML) {
24
+ el.innerHTML = innerHTML;
25
+ }
26
+ document.body.appendChild(el);
27
+ return el;
28
+ }
29
+
30
+ // Helper to get content root (shadowRoot or the element itself)
31
+ export function getContentRoot(el, shadow) {
32
+ if (shadow === 'none') {
33
+ return el;
34
+ }
35
+ return el.shadowRoot || el;
36
+ }
package/tsconfig.json CHANGED
@@ -7,7 +7,7 @@
7
7
  "noImplicitReturns": true,
8
8
  "noUnusedParameters": true,
9
9
  "strictFunctionTypes": true,
10
- //"strictNullChecks": true
10
+ "strictNullChecks": true
11
11
  },
12
12
  "exclude": [
13
13
  "node_modules",
@@ -0,0 +1,30 @@
1
+ import { playwrightLauncher } from '@web/test-runner-playwright';
2
+
3
+ export default {
4
+ files: 'test/**/*.test.js',
5
+ nodeResolve: true,
6
+ coverage: true,
7
+ coverageConfig: {
8
+ include: ['src/**/*.js'],
9
+ },
10
+ browserLogs: true,
11
+ browsers: [
12
+ playwrightLauncher({ product: 'chromium' }),
13
+ playwrightLauncher({ product: 'chromium', launchOptions: { channel: 'chrome' } }),
14
+ playwrightLauncher({ product: 'chromium', launchOptions: { channel: 'msedge' } }),
15
+ playwrightLauncher({ product: 'firefox' }),
16
+ playwrightLauncher({ product: 'webkit' }),
17
+ ],
18
+ testRunnerHtml: testFramework => `
19
+ <!DOCTYPE html>
20
+ <html>
21
+ <head>
22
+ <meta http-equiv="Content-Security-Policy" content="default-src 'self'" />
23
+ <script src="/src/decue.js"></script>
24
+ </head>
25
+ <body>
26
+ <script type="module" src="${testFramework}"></script>
27
+ </body>
28
+ </html>
29
+ `
30
+ };
package/serve.sh DELETED
@@ -1,4 +0,0 @@
1
- #! /usr/bin/env nix-shell
2
- #! nix-shell -I channel:nixos-22.11-small -i bash -p nodejs-16_x
3
-
4
- npx serve
@@ -1,84 +0,0 @@
1
- var assert = chai.assert;
2
-
3
- describe("decue", function() {
4
- beforeEach(function () {
5
- clearWorkArea();
6
- });
7
-
8
- ['none', 'open', 'closed'].forEach(shadow => {
9
- describe("shadowmode: " + shadow, function() {
10
- it('shadowroot', function () {
11
- make(`<my-element shadow="${shadow}"></my-element>`)
12
- if (shadow === 'open') {
13
- assert.isNotNull(document.querySelector('my-element').shadowRoot);
14
- } else {
15
- assert.isNull(document.querySelector('my-element').shadowRoot);
16
- }
17
- });
18
-
19
- it('no-attributes', function () {
20
- make(`<my-element></my-element>`)
21
- assert.isDefined(document.querySelector('my-element'));
22
- });
23
-
24
- it('unknown attribute placeholder is not replaced', function () {
25
- make(`<my-element></my-element>`)
26
- assert.equal('Target: {target}', document.querySelector('my-element span').getAttribute('data-target'));
27
- });
28
-
29
- it('attribute placeholder is replaced', function () {
30
- make(`<my-element target="World"></my-element>`)
31
- assert.equal('Target: World', document.querySelector('my-element span').getAttribute('data-target'));
32
- });
33
-
34
- it('attribute with placeholder and functions updates asynchronously', function (done) {
35
- make(`<my-element target="World"></my-element>`)
36
- assert.equal('WORLD', document.querySelector('my-element span').getAttribute('data-method'));
37
- document.querySelector('my-element').setAttribute('target', 'Universe');
38
- setTimeout(() => {
39
- assert.equal('UNIVERSE', document.querySelector('my-element span').getAttribute('data-method'));
40
- done();
41
- });
42
- });
43
-
44
- it('attribute with functions', function () {
45
- });
46
-
47
- it('attribute with method', function () {
48
- });
49
-
50
- it('text content placeholder is replaced', function () {
51
- make(`<my-element target="World"></my-element>`)
52
- assert.equal('Hello World', document.querySelector('my-element span').innerText);
53
- });
54
-
55
- it('text content updates synchronously for predefined elements', function () {
56
- make(`<my-predefined-element target="World"></my-predefined-element>`)
57
- assert.equal('Hello World', document.querySelector('my-predefined-element span').innerText);
58
- document.querySelector('my-predefined-element').setAttribute('target', 'Universe');
59
- assert.equal('Hello Universe', document.querySelector('my-predefined-element span').innerText);
60
- });
61
-
62
- it('named slot with default value', function () {
63
- });
64
-
65
- it('named slot with given value', function () {
66
- });
67
-
68
- it('default slot with default value', function () {
69
- });
70
-
71
- it('default slot with given value', function () {
72
- });
73
-
74
- it('multiple named slots', function () {
75
- });
76
-
77
- it('multiple default slots', function () {
78
- });
79
-
80
- it('some form tests...?', function () {
81
- });
82
- });
83
- });
84
- });
package/test/index.html DELETED
@@ -1,65 +0,0 @@
1
- <html lang="en">
2
- <head>
3
- <meta charset="utf-8" />
4
- <title>Mocha Tests</title>
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
- <link rel="stylesheet" href="../node_modules/mocha/mocha.css" />
7
- <meta http-equiv="cache-control" content="no-cache, must-revalidate, post-check=0, pre-check=0" />
8
- <meta http-equiv="cache-control" content="max-age=0" />
9
- <meta http-equiv="expires" content="0" />
10
- <meta http-equiv="expires" content="Tue, 01 Jan 1980 1:00:00 GMT" />
11
- <meta http-equiv="pragma" content="no-cache" />
12
- </head>
13
- <body style="padding:20px;font-family: sans-serif">
14
-
15
- <h1 style="margin-top: 40px">decue test suite</h1>
16
-
17
- <h2>Mocha Test Suite</h2>
18
- <a href="/test/">[ALL]</a>
19
-
20
- <script src="../node_modules/chai/chai.js"></script>
21
- <script src="../node_modules/mocha/mocha.js"></script>
22
- <script class="mocha-init">
23
- mocha.setup('bdd');
24
- mocha.checkLeaks();
25
- </script>
26
-
27
- <script src="util/util.js"></script>
28
-
29
- <script>
30
- function toLower(str) {
31
- str.toLowerCase();
32
- }
33
- </script>
34
-
35
- <script src="../src/decue.js" elements="my-predefined-element my-predefined-element-with-attributes[target]"></script>
36
- <script src="decue-tests.js"></script>
37
-
38
- <div id="mocha"></div>
39
-
40
- <script class="mocha-exec">
41
- mocha.run();
42
- </script>
43
- <em>Work Area</em>
44
- <hr/>
45
- <div id="work-area">
46
- </div>
47
-
48
- <template decue="nested-element">
49
- nested
50
- </template>
51
- <template decue="my-element">
52
- <span data-target="Target: {target}" data-method="{target|.toUpperCase}" data-function="{target|toLower}" data-piped="{target|.toUpperCase|toLower}">Hello {target}</span>
53
- <slot name="slot1">slot1</slot>
54
- <slot>default slot</slot>
55
- <nested-element></nested-element>
56
- </template>
57
- <template decue="my-predefined-element">
58
- <span>Hello {target}</span>
59
- </template>
60
- <template decue="my-predefined-element-with-attributes">
61
- <span>Hello {target}</span>
62
- </template>
63
-
64
- </body>
65
- </html>
package/test/util/util.js DELETED
@@ -1,17 +0,0 @@
1
-
2
- function make(htmlStr) {
3
- var range = document.createRange();
4
- var fragment = range.createContextualFragment(htmlStr);
5
- var wa = document.getElementById("work-area");
6
- var child = null;
7
- var children = fragment.children || fragment.childNodes; // IE
8
- while(children.length > 0) {
9
- child = children[0];
10
- wa.appendChild(child);
11
- }
12
- return child;
13
- }
14
-
15
- function clearWorkArea() {
16
- document.getElementById("work-area").innerHTML = "";
17
- }