@knowark/componarkjs 1.13.4 → 1.14.1

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.
Files changed (118) hide show
  1. package/README.md +57 -45
  2. package/lib/base/component/component.js +142 -20
  3. package/lib/base/component/component.test.js +753 -374
  4. package/lib/base/component/index.js +3 -0
  5. package/lib/base/styles/index.js +4 -1
  6. package/lib/base/utils/define.js +30 -7
  7. package/lib/base/utils/define.test.js +129 -42
  8. package/lib/base/utils/format.js +12 -6
  9. package/lib/base/utils/format.test.js +16 -16
  10. package/lib/base/utils/helpers.js +42 -9
  11. package/lib/base/utils/helpers.test.js +134 -115
  12. package/lib/base/utils/index.js +1 -0
  13. package/lib/base/utils/slots.js +3 -2
  14. package/lib/base/utils/slots.test.js +38 -38
  15. package/lib/base/utils/uuid.js +1 -1
  16. package/lib/base/utils/uuid.test.js +13 -13
  17. package/lib/components/audio/components/audio.js +36 -3
  18. package/lib/components/audio/components/audio.test.js +120 -90
  19. package/lib/components/audio/index.js +1 -0
  20. package/lib/components/audio/styles/index.js +5 -1
  21. package/lib/components/camera/components/camera.js +15 -0
  22. package/lib/components/camera/components/camera.test.js +96 -91
  23. package/lib/components/camera/index.js +1 -0
  24. package/lib/components/camera/styles/index.js +5 -1
  25. package/lib/components/capture/components/capture.js +48 -4
  26. package/lib/components/capture/components/capture.test.js +165 -97
  27. package/lib/components/capture/index.js +1 -0
  28. package/lib/components/droparea/components/droparea-preview.js +114 -19
  29. package/lib/components/droparea/components/droparea-preview.test.js +344 -80
  30. package/lib/components/droparea/components/droparea.js +82 -6
  31. package/lib/components/droparea/components/droparea.test.js +309 -299
  32. package/lib/components/droparea/index.js +1 -0
  33. package/lib/components/droparea/styles/index.js +5 -1
  34. package/lib/components/emit/components/emit.js +34 -4
  35. package/lib/components/emit/components/emit.test.js +192 -134
  36. package/lib/components/emit/index.js +1 -0
  37. package/lib/components/index.js +2 -1
  38. package/lib/components/list/components/item.js +6 -0
  39. package/lib/components/list/components/item.test.js +69 -68
  40. package/lib/components/list/components/list.js +51 -7
  41. package/lib/components/list/components/list.test.js +358 -227
  42. package/lib/components/list/index.js +1 -0
  43. package/lib/components/paginator/components/paginator.js +34 -8
  44. package/lib/components/paginator/components/paginator.test.js +146 -143
  45. package/lib/components/paginator/index.js +1 -0
  46. package/lib/components/paginator/styles/index.js +5 -1
  47. package/lib/components/spinner/components/spinner.js +10 -0
  48. package/lib/components/spinner/components/spinner.test.js +36 -41
  49. package/lib/components/spinner/index.js +1 -0
  50. package/lib/components/spinner/styles/index.js +5 -1
  51. package/lib/components/splitview/components/splitview.detail.js +10 -1
  52. package/lib/components/splitview/components/splitview.detail.test.js +75 -73
  53. package/lib/components/splitview/components/splitview.js +54 -11
  54. package/lib/components/splitview/components/splitview.master.js +37 -2
  55. package/lib/components/splitview/components/splitview.master.test.js +52 -52
  56. package/lib/components/splitview/components/splitview.test.js +135 -31
  57. package/lib/components/splitview/index.js +1 -0
  58. package/lib/components/translate/components/translate.js +65 -14
  59. package/lib/components/translate/components/translate.test.js +658 -131
  60. package/lib/components/translate/index.js +1 -0
  61. package/lib/index.js +3 -0
  62. package/package.json +4 -27
  63. package/scripts/node-test-setup.js +94 -0
  64. package/tsconfig.json +1 -1
  65. package/types/base/component/component.d.ts +43 -8
  66. package/types/base/component/component.d.ts.map +1 -1
  67. package/types/base/component/index.d.ts +4 -6
  68. package/types/base/component/index.d.ts.map +1 -1
  69. package/types/base/styles/index.d.ts +3 -2
  70. package/types/base/styles/index.d.ts.map +1 -1
  71. package/types/base/utils/define.d.ts +3 -2
  72. package/types/base/utils/define.d.ts.map +1 -1
  73. package/types/base/utils/format.d.ts +12 -6
  74. package/types/base/utils/format.d.ts.map +1 -1
  75. package/types/base/utils/helpers.d.ts +27 -7
  76. package/types/base/utils/helpers.d.ts.map +1 -1
  77. package/types/base/utils/slots.d.ts +8 -10
  78. package/types/base/utils/slots.d.ts.map +1 -1
  79. package/types/base/utils/uuid.d.ts +1 -1
  80. package/types/base/utils/uuid.d.ts.map +1 -1
  81. package/types/components/audio/components/audio.d.ts +23 -9
  82. package/types/components/audio/components/audio.d.ts.map +1 -1
  83. package/types/components/audio/styles/index.d.ts +3 -2
  84. package/types/components/audio/styles/index.d.ts.map +1 -1
  85. package/types/components/camera/components/camera.d.ts +11 -3
  86. package/types/components/camera/components/camera.d.ts.map +1 -1
  87. package/types/components/camera/styles/index.d.ts +3 -2
  88. package/types/components/camera/styles/index.d.ts.map +1 -1
  89. package/types/components/capture/components/capture.d.ts +23 -3
  90. package/types/components/capture/components/capture.d.ts.map +1 -1
  91. package/types/components/droparea/components/droparea-preview.d.ts +64 -11
  92. package/types/components/droparea/components/droparea-preview.d.ts.map +1 -1
  93. package/types/components/droparea/components/droparea.d.ts +58 -13
  94. package/types/components/droparea/components/droparea.d.ts.map +1 -1
  95. package/types/components/droparea/styles/index.d.ts +3 -2
  96. package/types/components/droparea/styles/index.d.ts.map +1 -1
  97. package/types/components/emit/components/emit.d.ts +15 -3
  98. package/types/components/emit/components/emit.d.ts.map +1 -1
  99. package/types/components/list/components/item.d.ts +8 -1
  100. package/types/components/list/components/item.d.ts.map +1 -1
  101. package/types/components/list/components/list.d.ts +23 -5
  102. package/types/components/list/components/list.d.ts.map +1 -1
  103. package/types/components/paginator/components/paginator.d.ts +32 -8
  104. package/types/components/paginator/components/paginator.d.ts.map +1 -1
  105. package/types/components/paginator/styles/index.d.ts +3 -2
  106. package/types/components/paginator/styles/index.d.ts.map +1 -1
  107. package/types/components/spinner/components/spinner.d.ts +14 -3
  108. package/types/components/spinner/components/spinner.d.ts.map +1 -1
  109. package/types/components/spinner/styles/index.d.ts +3 -2
  110. package/types/components/spinner/styles/index.d.ts.map +1 -1
  111. package/types/components/splitview/components/splitview.d.ts +22 -4
  112. package/types/components/splitview/components/splitview.d.ts.map +1 -1
  113. package/types/components/splitview/components/splitview.detail.d.ts +12 -2
  114. package/types/components/splitview/components/splitview.detail.d.ts.map +1 -1
  115. package/types/components/splitview/components/splitview.master.d.ts +12 -1
  116. package/types/components/splitview/components/splitview.master.d.ts.map +1 -1
  117. package/types/components/translate/components/translate.d.ts +44 -10
  118. package/types/components/translate/components/translate.d.ts.map +1 -1
@@ -1,4 +1,5 @@
1
- import { jest } from '@jest/globals'
1
+ import { it, mock } from 'node:test'
2
+ import assert from 'node:assert/strict'
2
3
  import { Component } from './component.js'
3
4
 
4
5
  class MockComponent extends Component {
@@ -42,486 +43,864 @@ class MockContentComponent extends Component {
42
43
  }
43
44
  Component.define('mock-content-component', MockContentComponent)
44
45
 
45
- describe('Component', () => {
46
- let container = null
47
- let component = null
48
- beforeEach(() => {
49
- container = document.createElement('div')
50
- container.innerHTML = '<mock-component code="XYZ123"></mock-component>'
51
- component = container.querySelector('mock-component')
52
- document.body.append(container)
53
- })
46
+ class AsyncLoadComponent extends Component {
47
+ async load () {
48
+ throw new Error('Async Load Error!')
49
+ }
50
+ }
51
+ Component.define('async-load-component', AsyncLoadComponent)
54
52
 
55
- afterEach(() => {
56
- container.remove()
57
- container = null
58
- component = null
59
- })
53
+ class SyncLoadComponent extends Component {
54
+ load () {
55
+ throw new Error('Sync Load Error!')
56
+ }
57
+ }
58
+ Component.define('sync-load-component', SyncLoadComponent)
60
59
 
61
- it('can be instantiated', () => {
62
- expect(component).toBeTruthy()
63
- })
60
+ class PlainLoadComponent extends Component {
61
+ load () {
62
+ return null
63
+ }
64
+ }
65
+ Component.define('plain-load-component', PlainLoadComponent)
64
66
 
65
- it('has an stable public api', () => {
66
- expect(typeof component.constructor.define).toEqual('function')
67
- expect(typeof component.init).toEqual('function')
68
- expect(typeof component.reflectedProperties).toEqual('function')
69
- expect(typeof component.connectedCallback).toEqual('function')
70
- expect(typeof component.render).toEqual('function')
71
- expect(typeof component.load).toEqual('function')
72
- expect(typeof component.select).toEqual('function')
73
- expect(typeof component.selectAll).toEqual('function')
74
- expect(typeof component.emit).toEqual('function')
75
- expect(typeof component.content).toBeDefined()
76
- })
67
+ class StringLoadComponent extends Component {
68
+ load () {
69
+ throw 'String Load Error!'
70
+ }
71
+ }
72
+ Component.define('string-load-component', StringLoadComponent)
77
73
 
78
- it('has an init method through which state is set', () => {
79
- const response = component.init({ attribute: 'value' })
80
- expect(response).toBe(component)
81
- })
74
+ class CleanupComponent extends Component {
75
+ init (context = {}) {
76
+ this.cleanupCalls = 0
77
+ return super.init(context)
78
+ }
82
79
 
83
- it('can set its content via a property', () => {
84
- component.content = '<p>Hello World</p>'
85
- const paragraph = component.querySelector('p')
86
- expect(component.content).toBe('<p>Hello World</p>')
87
- expect(paragraph.outerHTML).toBe('<p>Hello World</p>')
88
- })
80
+ render () {
81
+ return super.render()
82
+ }
83
+ }
84
+ Component.define('cleanup-component', CleanupComponent)
89
85
 
90
- it('can have some of its attributes reflected as properties', () => {
91
- expect(component.code).toBe('XYZ123')
92
- })
93
- it('has an asynchronous load method which is empty by default', async () => {
94
- expect(await component.load()).toBeUndefined()
95
- })
86
+ class OptimizedListenerComponent extends Component {
87
+ init (context = {}) {
88
+ this.clicks = 0
89
+ return super.init(context)
90
+ }
96
91
 
97
- it('sets its tag name as class when rendered', () => {
98
- component.render()
99
- expect(component.className).toEqual('mock-component')
100
- })
92
+ render () {
93
+ if (!this.querySelector('button')) {
94
+ this.content = '<button listen on-click="onClick">Click me</button>'
95
+ }
96
+ return super.render()
97
+ }
101
98
 
102
- it('keeps its previous classes after rendering', () => {
103
- component.classList.add('custom-class')
104
- component.classList.add('custom-class')
105
- component.classList.add('special-class')
106
- component.render()
107
- expect(component.className).toEqual(
108
- 'mock-component custom-class special-class')
109
- })
99
+ onClick () {
100
+ this.clicks += 1
101
+ }
102
+ }
103
+ Component.define('optimized-listener-component', OptimizedListenerComponent)
110
104
 
111
- it('emits custom events', () => {
112
- let detail = null
113
- const handler = (event) => { detail = event.detail }
114
- component.addEventListener('fire', handler)
105
+ let container = null
106
+ let component = null
115
107
 
116
- component.emit('fire', { location: 'indoors' })
108
+ const setup = () => {
109
+ document.body.innerHTML = ''
110
+ container = document.createElement('div')
111
+ container.innerHTML = '<mock-component code="XYZ123"></mock-component>'
112
+ component = container.querySelector('mock-component')
113
+ document.body.append(container)
114
+ }
117
115
 
118
- expect(detail).toEqual({ location: 'indoors' })
119
- })
116
+ it('can be instantiated', () => {
117
+ setup()
118
+ assert.ok(component)
119
+ })
120
120
 
121
- it('calls the load method on connectedCallback', async () => {
122
- const component = /** @type {Component} */ (
123
- document.createElement('mock-component'))
124
- const initSpy = jest.spyOn(component, 'init')
125
- const renderSpy = jest.spyOn(component, 'render')
126
- const loadSpy = jest.spyOn(component, 'load')
121
+ it('has an stable public api', () => {
122
+ setup()
123
+ assert.deepStrictEqual(typeof component.constructor.define, 'function')
124
+ assert.deepStrictEqual(typeof component.init, 'function')
125
+ assert.deepStrictEqual(typeof component.reflectedProperties, 'function')
126
+ assert.deepStrictEqual(typeof component.connectedCallback, 'function')
127
+ assert.deepStrictEqual(typeof component.render, 'function')
128
+ assert.deepStrictEqual(typeof component.load, 'function')
129
+ assert.deepStrictEqual(typeof component.select, 'function')
130
+ assert.deepStrictEqual(typeof component.selectAll, 'function')
131
+ assert.deepStrictEqual(typeof component.emit, 'function')
132
+ assert.notStrictEqual(typeof component.content, undefined)
133
+ })
127
134
 
128
- document.body.append(component)
135
+ it('has an init method through which state is set', () => {
136
+ setup()
137
+ const response = component.init({ attribute: 'value' })
138
+ assert.strictEqual(response, component)
139
+ })
129
140
 
130
- expect(initSpy).toHaveBeenCalledTimes(1)
131
- expect(renderSpy).toHaveBeenCalledTimes(1)
132
- expect(loadSpy).toHaveBeenCalledTimes(1)
133
- })
141
+ it('can set its content via a property', () => {
142
+ setup()
143
+ component.content = '<p>Hello World</p>'
144
+ const paragraph = component.querySelector('p')
145
+ assert.strictEqual(component.content, '<p>Hello World</p>')
146
+ assert.strictEqual(paragraph.outerHTML, '<p>Hello World</p>')
147
+ })
134
148
 
135
- it('catches and re-raises connectedCallback errors', async () => {
136
- expect.assertions(1)
137
- const component = /** @type {Component} */ (
138
- document.createElement('mock-component'))
139
- const consoleError = console.error
140
- console.error = jest.fn()
141
- component.render = () => {
142
- throw new Error('Render Error!')
143
- }
149
+ it('can have some of its attributes reflected as properties', () => {
150
+ setup()
151
+ assert.strictEqual(component.code, 'XYZ123')
152
+ })
153
+ it('has an asynchronous load method which is empty by default', async () => {
154
+ setup()
155
+ assert.strictEqual(await component.load(), undefined)
156
+ })
144
157
 
145
- try {
146
- component.connectedCallback()
147
- } catch (error) {
148
- expect(error.message).toEqual('Render Error!')
149
- }
158
+ it('sets its tag name as class when rendered', () => {
159
+ setup()
160
+ component.render()
161
+ assert.deepStrictEqual(component.className, 'mock-component')
162
+ })
150
163
 
151
- console.error = consoleError
152
- })
164
+ it('keeps its previous classes after rendering', () => {
165
+ setup()
166
+ component.classList.add('custom-class')
167
+ component.classList.add('custom-class')
168
+ component.classList.add('special-class')
169
+ component.render()
170
+ assert.deepStrictEqual(component.className, 'mock-component custom-class special-class')
171
+ })
153
172
 
154
- it('selects the children matching a selector', () => {
155
- container.innerHTML = `
156
- <mock-component>
157
- <div class="blue"></div>
158
- <div class="red"></div>
159
- <div class="red"></div>
160
- </mock-component>
161
- `
162
- const component = container.querySelector('mock-component')
173
+ it('emits custom events', () => {
174
+ setup()
175
+ let detail = null
176
+ const handler = (event) => { detail = event.detail }
177
+ component.addEventListener('fire', handler)
163
178
 
164
- const blue = component.select('.blue')
165
- expect(blue.tagName).toEqual('DIV')
179
+ component.emit('fire', { location: 'indoors' })
166
180
 
167
- const red = component.selectAll('.red')
168
- expect(red.length).toEqual(2)
169
- })
181
+ assert.deepStrictEqual(detail, { location: 'indoors' })
182
+ })
170
183
 
171
- it('retrieves its slots through the slots method', () => {
172
- container.innerHTML = `
173
- <mock-component>
174
- <div slot="header" class="header"></div>
175
- <div class="body"></div>
176
- <div class="aside"></div>
177
- <div slot="footer" class="footer"></div>
178
- </mock-component>
179
- `
184
+ it('calls the load method on connectedCallback', async () => {
185
+ setup()
186
+ const component = /** @type {Component} */ (
187
+ document.createElement('mock-component'))
188
+ const initSpy = mock.method(component, 'init')
189
+ const renderSpy = mock.method(component, 'render')
190
+ const loadSpy = mock.method(component, 'load')
191
+
192
+ document.body.append(component)
193
+
194
+ assert.strictEqual(initSpy.mock.calls.length, 1)
195
+ assert.strictEqual(renderSpy.mock.calls.length, 1)
196
+ assert.strictEqual(loadSpy.mock.calls.length, 1)
197
+ initSpy.mock.restore()
198
+ renderSpy.mock.restore()
199
+ loadSpy.mock.restore()
200
+ })
201
+
202
+ it('catches and re-raises connectedCallback errors', async () => {
203
+ setup()
204
+ const component = /** @type {Component} */ (
205
+ document.createElement('mock-component'))
206
+ const consoleErrorMock = mock.method(console, 'error', () => {})
207
+ let errorEvent = null
208
+ component.addEventListener('error', (event) => {
209
+ errorEvent = event
210
+ })
211
+ component.render = () => {
212
+ throw new Error('Render Error!')
213
+ }
214
+
215
+ try {
216
+ component.connectedCallback()
217
+ } catch (error) {
218
+ assert.deepStrictEqual(error.message, 'Render Error!')
219
+ assert.deepStrictEqual(errorEvent.detail.phase, 'init-render')
220
+ assert.deepStrictEqual(errorEvent.detail.component, 'MOCK-COMPONENT')
221
+ } finally {
222
+ consoleErrorMock.mock.restore()
223
+ }
224
+ })
225
+
226
+ it('creates events without CustomEvent and keeps event details', () => {
227
+ setup()
180
228
  const component = container.querySelector('mock-component')
229
+ const previousCustomEvent = globalThis.CustomEvent
181
230
 
182
- expect(component.slots).toEqual({
183
- header: [component.select('.header')],
184
- general: [component.select('.body'), component.select('.aside')],
185
- footer: [component.select('.footer')]
231
+ try {
232
+ globalThis.CustomEvent = undefined
233
+ let detail = null
234
+ component.addEventListener('fallback-emit', (event) => {
235
+ detail = event.detail
186
236
  })
237
+
238
+ component.emit('fallback-emit', { ready: true })
239
+
240
+ assert.deepStrictEqual(detail, { ready: true })
241
+ } finally {
242
+ globalThis.CustomEvent = previousCustomEvent
243
+ }
187
244
  })
188
245
 
189
- it('binds its properties to children events', async () => {
190
- container.innerHTML = `
191
- <mock-component>
192
- <input type="text" listen on-input="{{ data.value = data }}"></input>
193
- </mock-component>
194
- `
246
+ it('creates events from the component global window Event fallback', () => {
247
+ setup()
248
+ const component = document.createElement('mock-component')
249
+ const previousGlobal = component.global
195
250
 
196
- const component = container.querySelector('mock-component')
197
- const input = component.select('input')
251
+ try {
252
+ component.global = {
253
+ document: {
254
+ defaultView: {
255
+ Event: globalThis.Event
256
+ }
257
+ }
258
+ }
198
259
 
199
- input.dispatchEvent(
200
- new globalThis.InputEvent('input', { bubbles: true, data: 'E' })
201
- )
260
+ const event = component._createEvent('context-window-event', { ready: true })
202
261
 
203
- expect(component.data.value).toEqual('E')
262
+ assert.deepStrictEqual(event.type, 'context-window-event')
263
+ assert.deepStrictEqual(event.detail.ready, true)
264
+ assert.deepStrictEqual(typeof event.stopPropagation, 'function')
265
+ } finally {
266
+ component.global = previousGlobal
267
+ }
204
268
  })
205
269
 
206
- it('binds multiple handlers to the same event', async () => {
207
- container.innerHTML = `
208
- <mock-component>
209
- <input type="text"
210
- listen
211
- on-input="{{ data.value = data }}"
212
- on-input-1="{{ data.value1 = data }}"
213
- on-input-2="{{ data.value2 = data }}"
214
- on-input-3="{{ data.value3 = data }}"
215
- ></input>
216
- </mock-component>
217
- `
270
+ it('creates events from globalThis CustomEvent when component global has no window objects', () => {
271
+ setup()
272
+ const component = document.createElement('mock-component')
218
273
 
219
- const component = container.querySelector('mock-component')
220
- const input = component.select('input')
274
+ component.global = {}
275
+ const event = component._createEvent('global-custom-event', { ready: true })
221
276
 
222
- input.dispatchEvent(
223
- new globalThis.InputEvent('input', { bubbles: true, data: 'E' })
224
- )
277
+ assert.deepStrictEqual(event.type, 'global-custom-event')
278
+ assert.deepStrictEqual(event.detail.ready, true)
279
+ assert.deepStrictEqual(event instanceof globalThis.CustomEvent, true)
280
+ })
281
+
282
+ it('emits an error event when async load fails', async () => {
283
+ setup()
284
+ const component = document.createElement('async-load-component')
285
+ let errorEvent = null
225
286
 
226
- expect(component.data.value).toEqual('E')
227
- expect(component.data.value1).toEqual('E')
228
- expect(component.data.value2).toEqual('E')
229
- expect(component.data.value3).toEqual('E')
287
+ component.addEventListener('error', (event) => {
288
+ errorEvent = event
230
289
  })
231
290
 
232
- it('binds to the target.value event property by default', async () => {
233
- container.innerHTML = `
234
- <mock-component>
235
- <input type="text" listen on-input="{{ data.value }}"></input>
236
- </mock-component>
237
- `
238
- const component = container.querySelector('mock-component')
239
- const input = component.select('input')
240
- const inputEvent = new globalThis.InputEvent('input')
241
- const target = { name: 'input', value: 'X' }
242
- Object.defineProperty(
243
- inputEvent, 'target', { writable: false, value: target })
291
+ document.body.appendChild(component)
292
+
293
+ await new Promise(resolve => setTimeout(resolve, 0))
294
+
295
+ assert.ok(errorEvent)
296
+ assert.deepStrictEqual(errorEvent.detail.message, 'Async Load Error!')
297
+ assert.deepStrictEqual(errorEvent.detail.phase, 'load')
298
+ assert.deepStrictEqual(errorEvent.detail.component, 'ASYNC-LOAD-COMPONENT')
299
+ })
244
300
 
245
- input.dispatchEvent(inputEvent)
301
+ it('emits an error event and throws when sync load fails', () => {
302
+ setup()
303
+ const component = document.createElement('sync-load-component')
304
+ let errorEvent = null
246
305
 
247
- expect(component.data.value).toEqual('X')
306
+ component.addEventListener('error', (event) => {
307
+ errorEvent = event
248
308
  })
249
309
 
250
- it('binds to the detail custom event property', async () => {
251
- container.innerHTML = `
252
- <mock-component>
253
- <input type="text" listen on-alter="{{ data.value }}"></input>
254
- </mock-component>
255
- `
310
+ assert.throws(() => {
311
+ component.connectedCallback()
312
+ }, /Sync Load Error!/)
256
313
 
257
- const component = container.querySelector('mock-component')
258
- const input = component.select('input')
314
+ assert.ok(errorEvent)
315
+ assert.deepStrictEqual(errorEvent.detail.message, 'Sync Load Error!')
316
+ assert.deepStrictEqual(errorEvent.detail.phase, 'load')
317
+ assert.deepStrictEqual(errorEvent.detail.component, 'SYNC-LOAD-COMPONENT')
318
+ })
259
319
 
260
- input.dispatchEvent(
261
- new globalThis.CustomEvent('alter', { bubbles: true, detail: 'A' }))
320
+ it('coerces non-error load failures into errors with metadata', () => {
321
+ setup()
322
+ const component = document.createElement('string-load-component')
323
+ let errorEvent = null
262
324
 
263
- expect(component.data.value).toEqual('A')
325
+ component.addEventListener('error', (event) => {
326
+ errorEvent = event
264
327
  })
265
328
 
266
- it('performs basic transformations to event properties', async () => {
267
- container.innerHTML = `
268
- <mock-component>
269
- <input type="text" listen on-alter="{{ data.value | number }}"></input>
270
- </mock-component>
271
- `
329
+ assert.throws(() => {
330
+ component.connectedCallback()
331
+ }, /String Load Error!/)
272
332
 
273
- const component = container.querySelector('mock-component')
274
- const input = component.select('input')
333
+ assert.ok(errorEvent)
334
+ assert.deepStrictEqual(errorEvent.detail.message, 'String Load Error!')
335
+ assert.deepStrictEqual(errorEvent.detail.phase, 'load')
336
+ assert.deepStrictEqual(errorEvent.detail.component, 'STRING-LOAD-COMPONENT')
337
+ })
275
338
 
276
- input.dispatchEvent(
277
- new globalThis.CustomEvent('alter', { bubbles: true, detail: '777' }))
339
+ it('creates resolve events without CustomEvent and still resolves', () => {
340
+ setup()
341
+ const component = document.createElement('mock-component')
342
+ const previousCustomEvent = globalThis.CustomEvent
278
343
 
279
- expect(component.data.value).toEqual(777)
280
- })
344
+ try {
345
+ globalThis.CustomEvent = undefined
346
+ let resource = null
347
+ component.addEventListener('resolve', (event) => {
348
+ resource = event.detail.resource
349
+ })
281
350
 
282
- it('performs object assignment of event details', async () => {
283
- container.innerHTML = `
284
- <mock-component>
285
- <input type="text" listen on-alter="{{ local | object }}"></input>
286
- </mock-component>
287
- `
351
+ const result = component.resolve('dependency')
288
352
 
289
- const component = container.querySelector('mock-component')
290
- const input = component.select('input')
353
+ assert.deepStrictEqual(resource, 'dependency')
354
+ assert.strictEqual(result, undefined)
355
+ } finally {
356
+ globalThis.CustomEvent = previousCustomEvent
357
+ }
358
+ })
291
359
 
292
- const detail = { name: 'Sprite', brand: 'Coca-Cola' }
293
- input.dispatchEvent(
294
- new globalThis.CustomEvent('alter', { bubbles: true, detail }))
360
+ it('creates fallback events when neither CustomEvent nor Event are available', () => {
361
+ setup()
362
+ const component = document.createElement('mock-component')
363
+ const previousCustomEvent = globalThis.CustomEvent
364
+ const previousEvent = globalThis.Event
365
+
366
+ try {
367
+ globalThis.CustomEvent = undefined
368
+ globalThis.Event = undefined
369
+
370
+ component.global = {}
371
+ const event = component._createEvent('fallback', { ready: true })
372
+
373
+ assert.deepStrictEqual(event.type, 'fallback')
374
+ assert.deepStrictEqual(event.detail, { ready: true })
375
+ assert.deepStrictEqual(typeof event.stopPropagation, 'function')
376
+ assert.deepStrictEqual(typeof event.preventDefault, 'function')
377
+ assert.doesNotThrow(() => event.stopPropagation())
378
+ assert.doesNotThrow(() => event.preventDefault())
379
+ } finally {
380
+ globalThis.CustomEvent = previousCustomEvent
381
+ globalThis.Event = previousEvent
382
+ }
383
+ })
295
384
 
296
- expect(component.local).toEqual({ name: 'Sprite', brand: 'Coca-Cola' })
297
- })
385
+ it('falls back to globalThis when component global is missing', () => {
386
+ setup()
387
+ const component = document.createElement('mock-component')
388
+ const previousCustomEvent = globalThis.CustomEvent
298
389
 
299
- it('performs nested object assignment of event details', async () => {
300
- container.innerHTML = `
301
- <mock-component>
302
- <input type="text" listen on-alter="{{ local.zone | object }}"></input>
303
- </mock-component>
304
- `
390
+ try {
391
+ globalThis.CustomEvent = undefined
392
+ component.global = null
305
393
 
306
- const component = container.querySelector('mock-component')
307
- component.local.zone = {}
308
- const input = component.select('input')
394
+ const event = component._createEvent('global-fallback', { ready: true })
309
395
 
310
- const detail = { country: 'USA', city: 'Atlanta' }
311
- input.dispatchEvent(
312
- new globalThis.CustomEvent('alter', { bubbles: true, detail }))
396
+ assert.deepStrictEqual(event.type, 'global-fallback')
397
+ assert.deepStrictEqual(event.detail.ready, true)
398
+ } finally {
399
+ globalThis.CustomEvent = previousCustomEvent
400
+ }
401
+ })
313
402
 
314
- expect(component.local).toEqual({
315
- zone: {
316
- country: 'USA',
317
- city: 'Atlanta'
318
- }
319
- })
403
+ it('registers and runs cleanup callbacks on disconnect', () => {
404
+ setup()
405
+ const component = document.createElement('cleanup-component')
406
+ container.appendChild(component)
407
+
408
+ let cleanupCalls = 0
409
+ component.registerCleanup(() => {
410
+ cleanupCalls += 1
411
+ })
412
+ component.registerCleanup(() => {
413
+ cleanupCalls += 1
320
414
  })
321
415
 
322
- it('listens to events and handles them with its own methods', async () => {
323
- container.innerHTML = `
324
- <mock-component code="ABC123">
325
- <input type="text" listen on-alter="customHandler"></input>
326
- </mock-component>
327
- `
416
+ component.disconnectedCallback()
328
417
 
329
- const component = container.querySelector('mock-component')
330
- const input = component.select('input')
418
+ assert.deepStrictEqual(cleanupCalls, 2)
419
+ })
331
420
 
332
- const event = new globalThis.CustomEvent(
333
- 'alter', { bubbles: true, detail: { code: [] } })
334
- input.dispatchEvent(event)
421
+ it('supports unregistering cleanup callbacks', () => {
422
+ setup()
423
+ const component = document.createElement('cleanup-component')
424
+ container.appendChild(component)
335
425
 
336
- expect(event.detail.code).toEqual(['ABC123'])
426
+ let cleanupCalls = 0
427
+ const unregister = component.registerCleanup(() => {
428
+ cleanupCalls += 1
337
429
  })
430
+ unregister()
431
+ unregister()
338
432
 
339
- it('listens to events and redirects them to target components', async () => {
340
- container.innerHTML = `
341
- <mock-component code="ABC123">
342
- <input type="text" listen on-alter="customHandler@#child"></input>
343
- <mock-component id="child" code="XYZ890"></mock-component>
344
- </mock-component>
345
- `
433
+ component.disconnectedCallback()
346
434
 
347
- const component = container.querySelector('mock-component')
348
- const input = component.select('input')
435
+ assert.deepStrictEqual(cleanupCalls, 0)
436
+ })
349
437
 
350
- const event = new globalThis.CustomEvent(
351
- 'alter', { bubbles: true, detail: { code: [] } })
352
- input.dispatchEvent(event)
438
+ it('supports disconnected init fast-path when local state is already set', () => {
439
+ setup()
440
+ const component = document.createElement('mock-component')
441
+ component.local = { preset: true }
442
+ const initSpy = mock.method(component, 'init')
443
+ container.appendChild(component)
353
444
 
354
- expect(event.detail.code).toEqual(['XYZ890'])
355
- })
445
+ assert.deepStrictEqual(initSpy.mock.calls.length, 0)
446
+ initSpy.mock.restore()
447
+ })
356
448
 
357
- it('does not launch and event if the target is not found', async () => {
358
- container.innerHTML = `
359
- <mock-component code="ABC123">
360
- <input type="text" listen on-alter="customHandler@#missing"></input>
361
- <mock-component id="child" code="XYZ890"></mock-component>
362
- </mock-component>
363
- `
449
+ it('returns unchanged values when enhanced error details are absent', () => {
450
+ setup()
451
+ const component = document.createElement('cleanup-component')
364
452
 
365
- const component = container.querySelector('mock-component')
366
- const input = component.select('input')
453
+ const result = component._enhanceError(undefined, 'init-render')
454
+
455
+ assert.strictEqual(result, undefined)
456
+ })
457
+
458
+ it('accepts non-function cleanup callbacks as no-op registrations', () => {
459
+ setup()
460
+ const component = document.createElement('cleanup-component')
367
461
 
368
- const event = new globalThis.CustomEvent(
369
- 'alter', { bubbles: true, detail: { code: [] } })
370
- input.dispatchEvent(event)
462
+ const unregister = component.registerCleanup()
371
463
 
372
- expect(event.detail.code).toEqual([])
464
+ assert.doesNotThrow(() => unregister())
465
+ })
466
+
467
+ it('emits an error event when a cleanup callback throws', () => {
468
+ setup()
469
+ const component = document.createElement('cleanup-component')
470
+ container.appendChild(component)
471
+ let errorEvent = null
472
+
473
+ component.addEventListener('error', (event) => {
474
+ errorEvent = event
475
+ })
476
+ component.registerCleanup(() => {
477
+ throw new Error('Cleanup failed')
373
478
  })
374
479
 
480
+ component.disconnectedCallback()
375
481
 
376
- it('listens to clicks and redirects them to target components', async () => {
377
- container.innerHTML = `
378
- <mock-component code="ABC123">
379
- <div type="text" listen on-click="customHandler@#child">
380
- <button id="action">Action</button>
381
- </div>
382
- <mock-component id="child" code="XYZ890"></mock-component>
383
- </mock-component>
384
- `
482
+ assert.ok(errorEvent)
483
+ assert.deepStrictEqual(errorEvent.detail.message, 'Cleanup failed')
484
+ assert.deepStrictEqual(errorEvent.detail.phase, 'cleanup')
485
+ assert.deepStrictEqual(errorEvent.detail.component, 'CLEANUP-COMPONENT')
486
+ })
385
487
 
386
- const component = container.querySelector('mock-component')
387
- const button = component.select('#action')
488
+ it('supports load implementations that do not return promises', () => {
489
+ setup()
490
+ const component = document.createElement('plain-load-component')
491
+
492
+ assert.doesNotThrow(() => {
493
+ component.connectedCallback()
494
+ })
495
+ })
388
496
 
389
- const event = new globalThis.CustomEvent(
390
- 'click', { bubbles: true, detail: { code: [] } })
391
- button.dispatchEvent(event)
497
+ it('selects the children matching a selector', () => {
498
+ setup();
499
+ container.innerHTML = `
500
+ <mock-component>
501
+ <div class="blue"></div>
502
+ <div class="red"></div>
503
+ <div class="red"></div>
504
+ </mock-component>
505
+ `
506
+ const component = container.querySelector('mock-component')
507
+
508
+ const blue = component.select('.blue')
509
+ assert.deepStrictEqual(blue.tagName, 'DIV')
510
+
511
+ const red = component.selectAll('.red')
512
+ assert.deepStrictEqual(red.length, 2)
513
+ })
392
514
 
393
- expect(event.detail.code).toEqual(['XYZ890'])
515
+ it('retrieves its slots through the slots method', () => {
516
+ setup();
517
+ container.innerHTML = `
518
+ <mock-component>
519
+ <div slot="header" class="header"></div>
520
+ <div class="body"></div>
521
+ <div class="aside"></div>
522
+ <div slot="footer" class="footer"></div>
523
+ </mock-component>
524
+ `
525
+ const component = container.querySelector('mock-component')
526
+
527
+ assert.deepStrictEqual(component.slots, {
528
+ header: [component.select('.header')],
529
+ general: [component.select('.body'), component.select('.aside')],
530
+ footer: [component.select('.footer')]
394
531
  })
532
+ })
395
533
 
396
- it('listens to events and pipes them to target components', async () => {
397
- container.innerHTML = `
398
- <mock-component code="ABC123">
399
- <div type="text" listen on-change="replaceChildren%detail.name@#child">
400
- <button id="action">Action</button>
401
- </div>
402
- <div id="child">
403
- Hello
404
- </div>
405
- </mock-component>
406
- `
407
- const component = container.querySelector('mock-component')
408
- const button = component.select('#action')
534
+ it('binds its properties to children events', async () => {
535
+ setup();
536
+ container.innerHTML = `
537
+ <mock-component>
538
+ <input type="text" listen on-input="{{ data.value = data }}"></input>
539
+ </mock-component>
540
+ `
541
+
542
+ const component = container.querySelector('mock-component')
543
+ const input = component.select('input')
409
544
 
410
- const event = new globalThis.CustomEvent(
411
- 'change', { bubbles: true, detail: { name: 'World' } })
412
- button.dispatchEvent(event)
545
+ input.dispatchEvent(
546
+ new globalThis.InputEvent('input', { bubbles: true, data: 'E' })
547
+ )
413
548
 
414
- const child = container.querySelector('#child')
415
- expect(child.textContent.trim()).toEqual('World')
549
+ assert.deepStrictEqual(component.data.value, 'E')
550
+ })
551
+
552
+ it('binds multiple handlers to the same event', async () => {
553
+ setup();
554
+ container.innerHTML = `
555
+ <mock-component>
556
+ <input type="text"
557
+ listen
558
+ on-input="{{ data.value = data }}"
559
+ on-input-1="{{ data.value1 = data }}"
560
+ on-input-2="{{ data.value2 = data }}"
561
+ on-input-3="{{ data.value3 = data }}"
562
+ ></input>
563
+ </mock-component>
564
+ `
565
+
566
+ const component = container.querySelector('mock-component')
567
+ const input = component.select('input')
568
+
569
+ input.dispatchEvent(
570
+ new globalThis.InputEvent('input', { bubbles: true, data: 'E' })
571
+ )
572
+
573
+ assert.deepStrictEqual(component.data.value, 'E')
574
+ assert.deepStrictEqual(component.data.value1, 'E')
575
+ assert.deepStrictEqual(component.data.value2, 'E')
576
+ assert.deepStrictEqual(component.data.value3, 'E')
577
+ })
578
+
579
+ it('binds to the target.value event property by default', async () => {
580
+ setup()
581
+ container.innerHTML = `
582
+ <mock-component>
583
+ <input type="text" listen on-input="{{ data.value }}"></input>
584
+ </mock-component>
585
+ `
586
+ const component = container.querySelector('mock-component')
587
+ const input = component.select('input')
588
+ input.value = 'X'
589
+ const inputEvent = new globalThis.InputEvent('input', { bubbles: true })
590
+
591
+ input.dispatchEvent(inputEvent)
592
+
593
+ assert.deepStrictEqual(component.data.value, 'X')
594
+ })
595
+
596
+ it('binds to the detail custom event property', async () => {
597
+ setup();
598
+ container.innerHTML = `
599
+ <mock-component>
600
+ <input type="text" listen on-alter="{{ data.value }}"></input>
601
+ </mock-component>
602
+ `
603
+
604
+ const component = container.querySelector('mock-component')
605
+ const input = component.select('input')
606
+
607
+ input.dispatchEvent(
608
+ new globalThis.CustomEvent('alter', { bubbles: true, detail: 'A' }))
609
+
610
+ assert.deepStrictEqual(component.data.value, 'A')
611
+ })
612
+
613
+ it('performs basic transformations to event properties', async () => {
614
+ setup();
615
+ container.innerHTML = `
616
+ <mock-component>
617
+ <input type="text" listen on-alter="{{ data.value | number }}"></input>
618
+ </mock-component>
619
+ `
620
+
621
+ const component = container.querySelector('mock-component')
622
+ const input = component.select('input')
623
+
624
+ input.dispatchEvent(
625
+ new globalThis.CustomEvent('alter', { bubbles: true, detail: '777' }))
626
+
627
+ assert.deepStrictEqual(component.data.value, 777)
628
+ })
629
+
630
+ it('performs object assignment of event details', async () => {
631
+ setup();
632
+ container.innerHTML = `
633
+ <mock-component>
634
+ <input type="text" listen on-alter="{{ local | object }}"></input>
635
+ </mock-component>
636
+ `
637
+
638
+ const component = container.querySelector('mock-component')
639
+ const input = component.select('input')
640
+
641
+ const detail = { name: 'Sprite', brand: 'Coca-Cola' }
642
+ input.dispatchEvent(
643
+ new globalThis.CustomEvent('alter', { bubbles: true, detail }))
644
+
645
+ assert.deepStrictEqual(component.local, { name: 'Sprite', brand: 'Coca-Cola' })
646
+ })
647
+
648
+ it('performs nested object assignment of event details', async () => {
649
+ setup();
650
+ container.innerHTML = `
651
+ <mock-component>
652
+ <input type="text" listen on-alter="{{ local.zone | object }}"></input>
653
+ </mock-component>
654
+ `
655
+
656
+ const component = container.querySelector('mock-component')
657
+ component.local.zone = {}
658
+ const input = component.select('input')
659
+
660
+ const detail = { country: 'USA', city: 'Atlanta' }
661
+ input.dispatchEvent(
662
+ new globalThis.CustomEvent('alter', { bubbles: true, detail }))
663
+
664
+ assert.deepStrictEqual(component.local, {
665
+ zone: {
666
+ country: 'USA',
667
+ city: 'Atlanta'
668
+ }
416
669
  })
670
+ })
417
671
 
418
- it('emits an error event on declared listeners', async () => {
419
- container.innerHTML = `
420
- <mock-component>
421
- <input type="text" listen on-alter="erroringHandler"></input>
672
+ it('listens to events and handles them with its own methods', async () => {
673
+ setup();
674
+ container.innerHTML = `
675
+ <mock-component code="ABC123">
676
+ <input type="text" listen on-alter="customHandler"></input>
422
677
  </mock-component>
423
- `
678
+ `
424
679
 
425
- const component = container.querySelector('mock-component')
680
+ const component = container.querySelector('mock-component')
681
+ const input = component.select('input')
682
+
683
+ const event = new globalThis.CustomEvent(
684
+ 'alter', { bubbles: true, detail: { code: [] } })
685
+ input.dispatchEvent(event)
426
686
 
427
- let errorEvent = {}
428
- component.addEventListener('error', (event) => { errorEvent = event })
687
+ assert.deepStrictEqual(event.detail.code, ['ABC123'])
688
+ })
429
689
 
430
- const input = component.select('input')
690
+ it('listens to events and redirects them to target components', async () => {
691
+ setup();
692
+ container.innerHTML = `
693
+ <mock-component code="ABC123">
694
+ <input type="text" listen on-alter="customHandler@#child"></input>
695
+ <mock-component id="child" code="XYZ890"></mock-component>
696
+ </mock-component>
697
+ `
431
698
 
432
- input.dispatchEvent(
433
- new globalThis.CustomEvent(
434
- 'alter', { bubbles: true, detail: 'I will error!' }
435
- ))
699
+ const component = container.querySelector('mock-component')
700
+ const input = component.select('input')
436
701
 
437
- expect(errorEvent.detail.message).toEqual('Something went wrong!')
438
- })
702
+ const event = new globalThis.CustomEvent(
703
+ 'alter', { bubbles: true, detail: { code: [] } })
704
+ input.dispatchEvent(event)
439
705
 
440
- it('emits an error event on declared async listeners', async () => {
441
- container.innerHTML = `
442
- <mock-component>
443
- <input type="text" listen on-alter="asyncErroringHandler"></input>
706
+ assert.deepStrictEqual(event.detail.code, ['XYZ890'])
707
+ })
708
+
709
+ it('does not launch and event if the target is not found', async () => {
710
+ setup();
711
+ container.innerHTML = `
712
+ <mock-component code="ABC123">
713
+ <input type="text" listen on-alter="customHandler@#missing"></input>
714
+ <mock-component id="child" code="XYZ890"></mock-component>
444
715
  </mock-component>
445
- `
716
+ `
446
717
 
447
- const component = container.querySelector('mock-component')
718
+ const component = container.querySelector('mock-component')
719
+ const input = component.select('input')
448
720
 
449
- let errorEvent = {}
450
- component.addEventListener('error', (event) => { errorEvent = event })
721
+ const event = new globalThis.CustomEvent(
722
+ 'alter', { bubbles: true, detail: { code: [] } })
723
+ input.dispatchEvent(event)
451
724
 
452
- const input = component.select('input')
725
+ assert.deepStrictEqual(event.detail.code, [])
726
+ })
453
727
 
454
- input.dispatchEvent(
455
- new globalThis.CustomEvent(
456
- 'alter', { bubbles: true, detail: 'I will error!' }
457
- ))
458
728
 
459
- // Sleep
460
- await new Promise(resolve => setTimeout(resolve, 0))
729
+ it('listens to clicks and redirects them to target components', async () => {
730
+ setup();
731
+ container.innerHTML = `
732
+ <mock-component code="ABC123">
733
+ <div type="text" listen on-click="customHandler@#child">
734
+ <button id="action">Action</button>
735
+ </div>
736
+ <mock-component id="child" code="XYZ890"></mock-component>
737
+ </mock-component>
738
+ `
461
739
 
462
- expect(errorEvent.detail.message).toEqual('Something went async wrong!')
463
- })
740
+ const component = container.querySelector('mock-component')
741
+ const button = component.select('#action')
464
742
 
465
- it('resolves its resource dependencies using events propagation', () => {
466
- const listener = (event) => {
467
- const resource = event.detail.resource
468
- event.detail[resource] = 'RESOLVED_DEPENDENCY'
469
- }
470
- document.addEventListener('resolve', listener)
471
- container.innerHTML = `
472
- <mock-component></mock-component>
473
- `
474
- const component = container.querySelector('mock-component')
743
+ const event = new globalThis.CustomEvent(
744
+ 'click', { bubbles: true, detail: { code: [] } })
745
+ button.dispatchEvent(event)
475
746
 
476
- expect(component.dependency).toEqual('RESOLVED_DEPENDENCY')
477
- document.removeEventListener('resolve', listener)
478
- })
747
+ assert.deepStrictEqual(event.detail.code, ['XYZ890'])
748
+ })
479
749
 
480
- it('provides the dependencies requested to it by child components', () => {
481
- class ParentComponent extends MockComponent {
482
- provide (resource) {
483
- if (resource === 'Dependency') {
484
- return `RESOURCE: ${resource} PROVIDED BY: ${this.id}`
485
- }
486
- if (resource === 'state') return { key: 'value' }
750
+ it('listens to events and pipes them to target components', async () => {
751
+ setup();
752
+ container.innerHTML = `
753
+ <mock-component code="ABC123">
754
+ <div type="text" listen on-change="replaceChildren%detail.name@#child">
755
+ <button id="action">Action</button>
756
+ </div>
757
+ <div id="child">
758
+ Hello
759
+ </div>
760
+ </mock-component>
761
+ `
762
+ const component = container.querySelector('mock-component')
763
+ const button = component.select('#action')
764
+
765
+ const event = new globalThis.CustomEvent(
766
+ 'change', { bubbles: true, detail: { name: 'World' } })
767
+ button.dispatchEvent(event)
768
+
769
+ const child = container.querySelector('#child')
770
+ assert.deepStrictEqual(child.textContent.trim(), 'World')
771
+ })
772
+
773
+ it('emits an error event on declared listeners', async () => {
774
+ setup();
775
+ container.innerHTML = `
776
+ <mock-component>
777
+ <input type="text" listen on-alter="erroringHandler"></input>
778
+ </mock-component>
779
+ `
780
+
781
+ const component = container.querySelector('mock-component')
782
+
783
+ let errorEvent = {}
784
+ component.addEventListener('error', (event) => { errorEvent = event })
785
+
786
+ const input = component.select('input')
787
+
788
+ input.dispatchEvent(
789
+ new globalThis.CustomEvent(
790
+ 'alter', { bubbles: true, detail: 'I will error!' }
791
+ ))
792
+
793
+ assert.deepStrictEqual(errorEvent.detail.message, 'Something went wrong!')
794
+ })
795
+
796
+ it('emits an error event on declared async listeners', async () => {
797
+ setup();
798
+ container.innerHTML = `
799
+ <mock-component>
800
+ <input type="text" listen on-alter="asyncErroringHandler"></input>
801
+ </mock-component>
802
+ `
803
+
804
+ const component = container.querySelector('mock-component')
805
+
806
+ let errorEvent = {}
807
+ component.addEventListener('error', (event) => { errorEvent = event })
808
+
809
+ const input = component.select('input')
810
+
811
+ input.dispatchEvent(
812
+ new globalThis.CustomEvent(
813
+ 'alter', { bubbles: true, detail: 'I will error!' }
814
+ ))
815
+
816
+ // Sleep
817
+ await new Promise(resolve => setTimeout(resolve, 0))
818
+
819
+ assert.deepStrictEqual(errorEvent.detail.message, 'Something went async wrong!')
820
+ })
821
+
822
+ it('resolves its resource dependencies using events propagation', () => {
823
+ setup();
824
+ const listener = (event) => {
825
+ const resource = event.detail.resource
826
+ event.detail[resource] = 'RESOLVED_DEPENDENCY'
827
+ }
828
+ document.addEventListener('resolve', listener)
829
+ container.innerHTML = `
830
+ <mock-component></mock-component>
831
+ `
832
+ const component = container.querySelector('mock-component')
833
+
834
+ assert.deepStrictEqual(component.dependency, 'RESOLVED_DEPENDENCY')
835
+ document.removeEventListener('resolve', listener)
836
+ })
837
+
838
+ it('provides the dependencies requested to it by child components', () => {
839
+ setup();
840
+ class ParentComponent extends MockComponent {
841
+ provide (resource) {
842
+ if (resource === 'Dependency') {
843
+ return `RESOURCE: ${resource} PROVIDED BY: ${this.id}`
487
844
  }
845
+ if (resource === 'state') return { key: 'value' }
488
846
  }
489
- Component.define('parent-component', ParentComponent)
847
+ }
848
+ Component.define('parent-component', ParentComponent)
490
849
 
491
- container.innerHTML = `
492
- <parent-component id="parent">
493
- <mock-component id="child"></mock-component>
494
- </parent-component>
495
- `
850
+ container.innerHTML = `
851
+ <parent-component id="parent">
852
+ <mock-component id="child"></mock-component>
853
+ </parent-component>
854
+ `
496
855
 
497
- const child = container.querySelector('#child')
856
+ const child = container.querySelector('#child')
498
857
 
499
- expect(child.dependency).toEqual(
500
- 'RESOURCE: Dependency PROVIDED BY: parent')
858
+ assert.deepStrictEqual(child.dependency, 'RESOURCE: Dependency PROVIDED BY: parent')
501
859
 
502
- const state = child.resolve('state')
503
- expect(state).toEqual({ key: 'value' })
860
+ const state = child.resolve('state')
861
+ assert.deepStrictEqual(state, { key: 'value' })
504
862
 
505
- const unknown = child.resolve('unknown')
506
- expect(unknown).toBe(undefined)
507
- })
863
+ const unknown = child.resolve('unknown')
864
+ assert.strictEqual(unknown, undefined)
865
+ })
508
866
 
509
- it('provides a styleNames utility function for setting styles', () => {
510
- container.innerHTML = `
511
- <mock-component class></mock-component>
512
- `
513
- const component = container.querySelector('mock-component')
514
- const background = 'primary'
515
- const shadow = 'small'
516
- const color = ''
517
- const styleMap = {
518
- [`background-${background}`]: background,
519
- [`color-${color}`]: color,
520
- [`shadow-${shadow}`]: shadow
521
- }
867
+ it('provides a styleNames utility function for setting styles', () => {
868
+ setup();
869
+ container.innerHTML = `
870
+ <mock-component class></mock-component>
871
+ `
872
+ const component = container.querySelector('mock-component')
873
+ const background = 'primary'
874
+ const shadow = 'small'
875
+ const color = ''
876
+ const styleMap = {
877
+ [`background-${background}`]: background,
878
+ [`color-${color}`]: color,
879
+ [`shadow-${shadow}`]: shadow
880
+ }
522
881
 
523
- const result = component.styleNames(styleMap)
882
+ const result = component.styleNames(styleMap)
524
883
 
525
- expect(result).toEqual('background-primary shadow-small')
526
- })
884
+ assert.deepStrictEqual(result, 'background-primary shadow-small')
885
+ })
886
+
887
+ it('reuses listener bindings when content does not change', () => {
888
+ setup()
889
+ container.innerHTML = `
890
+ <optimized-listener-component></optimized-listener-component>
891
+ `
892
+ const component = container.querySelector('optimized-listener-component')
893
+
894
+ component.render()
895
+ component.querySelector('button').click()
896
+
897
+ component.render()
898
+ component.querySelector('button').click()
899
+
900
+ component.content = '<button listen on-click="onClick">Click me</button>'
901
+ component.render()
902
+ component.querySelector('button').click()
903
+
904
+ assert.deepStrictEqual(component.clicks, 3)
905
+ assert.deepStrictEqual(component._needsBinding, false)
527
906
  })