@wippy-fe/webcomponent-core 0.0.34 → 0.0.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +452 -0
- package/dist/index.js +485 -0
- package/dist/index.js.map +1 -0
- package/package.json +21 -9
- package/src/base-element.host.test.ts +0 -310
- package/src/base-element.ts +0 -399
- package/src/css-loader.ts +0 -52
- package/src/host-bus.ts +0 -44
- package/src/host-scope.test.ts +0 -19
- package/src/host-scope.ts +0 -28
- package/src/index.ts +0 -22
- package/src/prop-parser.ts +0 -91
- package/src/reactive-adapter.test.ts +0 -290
- package/src/reactive-adapter.ts +0 -220
- package/src/types.ts +0 -130
|
@@ -1,310 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @vitest-environment jsdom
|
|
3
|
-
*
|
|
4
|
-
* Covers `WippyElement.host` getter — reads `__wippyHost` attached by
|
|
5
|
-
* the managed-layout shell's content resolvers. And the cleanup path
|
|
6
|
-
* in `disconnectedCallback` that drops stale wires on detach.
|
|
7
|
-
*/
|
|
8
|
-
import { describe, expect, it, vi } from 'vitest'
|
|
9
|
-
import { WippyElement } from './base-element.ts'
|
|
10
|
-
|
|
11
|
-
// `ElementInternals.states` shim lives in `vitest.setup.ts` (gen-2-chat
|
|
12
|
-
// repo root) so any test that transitively imports `WippyElement` gets
|
|
13
|
-
// the patch. This file just exercises the resulting behavior.
|
|
14
|
-
|
|
15
|
-
class TestEl extends WippyElement {
|
|
16
|
-
static get wippyConfig() {
|
|
17
|
-
return { propsSchema: { properties: {} }, hostCssKeys: [] }
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
protected onMount(): void {}
|
|
21
|
-
protected onUnmount(): void {}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const TAG = `wippy-host-test-${Math.random().toString(36).slice(2)}`
|
|
25
|
-
customElements.define(TAG, TestEl)
|
|
26
|
-
|
|
27
|
-
describe('wippyElement.host getter', () => {
|
|
28
|
-
it('returns null on a fresh element with no wires', () => {
|
|
29
|
-
const el = document.createElement(TAG) as TestEl
|
|
30
|
-
expect(el.host).toBeNull()
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
it('returns the wrapper attached by the resolver as __wippyHost', () => {
|
|
34
|
-
const el = document.createElement(TAG) as TestEl
|
|
35
|
-
const fakeWrapper = { layout: { broadcast: () => {} } }
|
|
36
|
-
;(el as unknown as { __wippyHost: unknown }).__wippyHost = fakeWrapper
|
|
37
|
-
expect(el.host).toBe(fakeWrapper)
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
it('connectedCallback can read this.host on first tick', () => {
|
|
41
|
-
let observedHost: unknown = 'sentinel'
|
|
42
|
-
class Probe extends WippyElement {
|
|
43
|
-
static get wippyConfig() {
|
|
44
|
-
return { propsSchema: { properties: {} }, hostCssKeys: [] }
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
protected onMount(): void {
|
|
48
|
-
observedHost = this.host
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
protected onUnmount(): void {}
|
|
52
|
-
}
|
|
53
|
-
const probeTag = `wippy-probe-${Math.random().toString(36).slice(2)}`
|
|
54
|
-
customElements.define(probeTag, Probe)
|
|
55
|
-
const el = document.createElement(probeTag)
|
|
56
|
-
const fakeWrapper = { layout: {} }
|
|
57
|
-
;(el as unknown as { __wippyHost: unknown }).__wippyHost = fakeWrapper
|
|
58
|
-
document.body.appendChild(el)
|
|
59
|
-
expect(observedHost).toBe(fakeWrapper)
|
|
60
|
-
el.remove()
|
|
61
|
-
})
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
describe('wippyElement.disconnectedCallback wire cleanup', () => {
|
|
65
|
-
it('drops __wippyHost and __wippyHostBus on disconnect', () => {
|
|
66
|
-
const el = document.createElement(TAG) as TestEl
|
|
67
|
-
;(el as unknown as { __wippyHost: unknown }).__wippyHost = { layout: {} }
|
|
68
|
-
;(el as unknown as { __wippyHostBus: unknown }).__wippyHostBus = {
|
|
69
|
-
broadcast: () => {},
|
|
70
|
-
send: () => {},
|
|
71
|
-
subscribe: () => () => {},
|
|
72
|
-
}
|
|
73
|
-
document.body.appendChild(el)
|
|
74
|
-
el.remove()
|
|
75
|
-
expect((el as unknown as { __wippyHost?: unknown }).__wippyHost).toBeUndefined()
|
|
76
|
-
expect((el as unknown as { __wippyHostBus?: unknown }).__wippyHostBus).toBeUndefined()
|
|
77
|
-
})
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
describe('wippyElement.reactive — opt-in reactive adapter', () => {
|
|
81
|
-
// Element with a `count` schema prop so attribute changes are observable.
|
|
82
|
-
interface ReactiveProps { count: number }
|
|
83
|
-
interface ReactiveEvents { tick: { n: number } }
|
|
84
|
-
class ReactiveEl extends WippyElement<ReactiveProps, ReactiveEvents> {
|
|
85
|
-
static get wippyConfig() {
|
|
86
|
-
return {
|
|
87
|
-
propsSchema: { properties: { count: { type: 'integer' as const } } },
|
|
88
|
-
hostCssKeys: [],
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/** Test-only handle for assertions inside lifecycle hooks. */
|
|
93
|
-
public observedHook: { props: ReactiveProps, errors: string[] } | null = null
|
|
94
|
-
/** True iff the adapter has been lazy-allocated. */
|
|
95
|
-
public get adapterAllocated(): boolean {
|
|
96
|
-
return (this as unknown as { _reactive: unknown })._reactive !== null
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
protected onMount(): void {}
|
|
100
|
-
protected onUnmount(): void {}
|
|
101
|
-
|
|
102
|
-
protected onPropsChanged(props: ReactiveProps, errors: string[]): void {
|
|
103
|
-
this.observedHook = { props, errors }
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
const reactiveTag = `wippy-reactive-${Math.random().toString(36).slice(2)}`
|
|
107
|
-
customElements.define(reactiveTag, ReactiveEl)
|
|
108
|
-
|
|
109
|
-
it('does NOT allocate the adapter when never accessed', () => {
|
|
110
|
-
const el = document.createElement(reactiveTag) as ReactiveEl
|
|
111
|
-
el.setAttribute('count', '1')
|
|
112
|
-
document.body.appendChild(el)
|
|
113
|
-
el.setAttribute('count', '2')
|
|
114
|
-
expect(el.adapterAllocated).toBe(false)
|
|
115
|
-
expect(el.observedHook?.props.count).toBe(2)
|
|
116
|
-
el.remove()
|
|
117
|
-
})
|
|
118
|
-
|
|
119
|
-
it('lazy-allocates on first `this.reactive` access and seeds from current attributes', () => {
|
|
120
|
-
const el = document.createElement(reactiveTag) as ReactiveEl
|
|
121
|
-
el.setAttribute('count', '7')
|
|
122
|
-
document.body.appendChild(el)
|
|
123
|
-
expect(el.adapterAllocated).toBe(false)
|
|
124
|
-
|
|
125
|
-
const reactive = (el as unknown as { reactive: ReturnType<ReactiveEl['onPropsChanged']> extends void ? any : never }).reactive
|
|
126
|
-
expect(el.adapterAllocated).toBe(true)
|
|
127
|
-
expect((reactive as any).props.value).toEqual({ count: 7 })
|
|
128
|
-
el.remove()
|
|
129
|
-
})
|
|
130
|
-
|
|
131
|
-
it('fires subscribers on attribute change after allocation', () => {
|
|
132
|
-
const el = document.createElement(reactiveTag) as ReactiveEl
|
|
133
|
-
el.setAttribute('count', '1')
|
|
134
|
-
document.body.appendChild(el)
|
|
135
|
-
|
|
136
|
-
const reactive = (el as any).reactive
|
|
137
|
-
const listener = vi.fn()
|
|
138
|
-
reactive.props.subscribe(listener)
|
|
139
|
-
|
|
140
|
-
el.setAttribute('count', '2')
|
|
141
|
-
expect(listener).toHaveBeenCalledTimes(1)
|
|
142
|
-
expect(listener).toHaveBeenCalledWith({ count: 2 }, [])
|
|
143
|
-
|
|
144
|
-
el.setAttribute('count', '3')
|
|
145
|
-
expect(listener).toHaveBeenCalledTimes(2)
|
|
146
|
-
el.remove()
|
|
147
|
-
})
|
|
148
|
-
|
|
149
|
-
it('events.emit dispatches a bubbling/composed CustomEvent on the element', () => {
|
|
150
|
-
const el = document.createElement(reactiveTag) as ReactiveEl
|
|
151
|
-
document.body.appendChild(el)
|
|
152
|
-
|
|
153
|
-
const reactive = (el as any).reactive
|
|
154
|
-
const events: Event[] = []
|
|
155
|
-
el.addEventListener('tick', (e) => {
|
|
156
|
-
events.push(e)
|
|
157
|
-
})
|
|
158
|
-
|
|
159
|
-
reactive.events.emit('tick', { n: 99 })
|
|
160
|
-
expect(events).toHaveLength(1)
|
|
161
|
-
const ev = events[0] as CustomEvent<{ n: number }>
|
|
162
|
-
expect(ev.detail).toEqual({ n: 99 })
|
|
163
|
-
expect(ev.bubbles).toBe(true)
|
|
164
|
-
expect(ev.composed).toBe(true)
|
|
165
|
-
el.remove()
|
|
166
|
-
})
|
|
167
|
-
|
|
168
|
-
it('disconnectedCallback disposes the adapter; subscribers stop firing', () => {
|
|
169
|
-
const el = document.createElement(reactiveTag) as ReactiveEl
|
|
170
|
-
document.body.appendChild(el)
|
|
171
|
-
|
|
172
|
-
const reactive = (el as any).reactive
|
|
173
|
-
const listener = vi.fn()
|
|
174
|
-
reactive.props.subscribe(listener)
|
|
175
|
-
|
|
176
|
-
el.remove()
|
|
177
|
-
expect(el.adapterAllocated).toBe(false)
|
|
178
|
-
|
|
179
|
-
// Re-append with a new attribute; the OLD subscriber must not fire.
|
|
180
|
-
el.setAttribute('count', '50')
|
|
181
|
-
document.body.appendChild(el)
|
|
182
|
-
el.setAttribute('count', '51')
|
|
183
|
-
expect(listener).not.toHaveBeenCalled()
|
|
184
|
-
el.remove()
|
|
185
|
-
})
|
|
186
|
-
|
|
187
|
-
it('after reconnect, a fresh adapter is allocated on next .reactive access', () => {
|
|
188
|
-
const el = document.createElement(reactiveTag) as ReactiveEl
|
|
189
|
-
document.body.appendChild(el)
|
|
190
|
-
|
|
191
|
-
const first = (el as any).reactive
|
|
192
|
-
el.remove()
|
|
193
|
-
document.body.appendChild(el)
|
|
194
|
-
|
|
195
|
-
const second = (el as any).reactive
|
|
196
|
-
expect(second).not.toBe(first)
|
|
197
|
-
el.remove()
|
|
198
|
-
})
|
|
199
|
-
|
|
200
|
-
it('legacy onPropsChanged override still fires alongside reactive', () => {
|
|
201
|
-
const el = document.createElement(reactiveTag) as ReactiveEl
|
|
202
|
-
el.setAttribute('count', '1')
|
|
203
|
-
document.body.appendChild(el)
|
|
204
|
-
// Force-allocate adapter
|
|
205
|
-
const reactive = (el as any).reactive
|
|
206
|
-
const listener = vi.fn()
|
|
207
|
-
reactive.props.subscribe(listener)
|
|
208
|
-
|
|
209
|
-
el.setAttribute('count', '2')
|
|
210
|
-
expect(listener).toHaveBeenCalledTimes(1)
|
|
211
|
-
expect(el.observedHook?.props.count).toBe(2)
|
|
212
|
-
el.remove()
|
|
213
|
-
})
|
|
214
|
-
})
|
|
215
|
-
|
|
216
|
-
describe('wippyElement reconnect — no shadow-child accumulation', () => {
|
|
217
|
-
// R3 regression: previously `connectedCallback` re-ran the full mount
|
|
218
|
-
// path (CSS injection + container append) on every reconnect. The
|
|
219
|
-
// `disconnectedCallback` did NOT clear the persistent shadow root, so
|
|
220
|
-
// each detach+reattach cycle leaked another `<style>` and another
|
|
221
|
-
// container `<div>`. On a move-style DOM op (e.g. `appendChild` of an
|
|
222
|
-
// already-connected element, list reorder) the browser fires
|
|
223
|
-
// disconnect → connect synchronously, so this leak fires on every
|
|
224
|
-
// reorder.
|
|
225
|
-
//
|
|
226
|
-
// Fix: gate CSS / container / icon setup on a `_initialized` flag and
|
|
227
|
-
// reuse the stored container on reconnect.
|
|
228
|
-
|
|
229
|
-
class CssEl extends WippyElement {
|
|
230
|
-
static get wippyConfig() {
|
|
231
|
-
return {
|
|
232
|
-
propsSchema: { properties: {} },
|
|
233
|
-
hostCssKeys: [],
|
|
234
|
-
inlineCss: ':host { display: block; }',
|
|
235
|
-
containerClasses: ['ct'],
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
public mountCount = 0
|
|
240
|
-
public lastIsRemount: boolean | undefined = undefined
|
|
241
|
-
public lastContainer: HTMLElement | null = null
|
|
242
|
-
|
|
243
|
-
protected onMount(
|
|
244
|
-
_shadow: ShadowRoot,
|
|
245
|
-
container: HTMLElement,
|
|
246
|
-
_props: Record<string, unknown>,
|
|
247
|
-
_errors: string[],
|
|
248
|
-
_content?: string | null,
|
|
249
|
-
isRemount?: boolean,
|
|
250
|
-
): void {
|
|
251
|
-
this.mountCount += 1
|
|
252
|
-
this.lastIsRemount = isRemount
|
|
253
|
-
this.lastContainer = container
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
protected onUnmount(): void {}
|
|
257
|
-
}
|
|
258
|
-
const cssTag = `wippy-reconnect-${Math.random().toString(36).slice(2)}`
|
|
259
|
-
customElements.define(cssTag, CssEl)
|
|
260
|
-
|
|
261
|
-
it('does not duplicate <style> or container <div> across detach/reattach cycles', () => {
|
|
262
|
-
const el = document.createElement(cssTag) as CssEl
|
|
263
|
-
document.body.appendChild(el)
|
|
264
|
-
|
|
265
|
-
const shadow = el.shadowRoot!
|
|
266
|
-
const initialChildCount = shadow.childElementCount
|
|
267
|
-
const initialStyles = shadow.querySelectorAll('style').length
|
|
268
|
-
const initialContainers = shadow.querySelectorAll('div.ct').length
|
|
269
|
-
expect(initialContainers).toBe(1)
|
|
270
|
-
expect(initialStyles).toBeGreaterThan(0)
|
|
271
|
-
expect(el.mountCount).toBe(1)
|
|
272
|
-
expect(el.lastIsRemount).toBe(false)
|
|
273
|
-
|
|
274
|
-
// Detach + reattach (one cycle) — pre-fix this would add another
|
|
275
|
-
// <style> and another container <div>.
|
|
276
|
-
el.remove()
|
|
277
|
-
document.body.appendChild(el)
|
|
278
|
-
|
|
279
|
-
expect(shadow.childElementCount).toBe(initialChildCount)
|
|
280
|
-
expect(shadow.querySelectorAll('style').length).toBe(initialStyles)
|
|
281
|
-
expect(shadow.querySelectorAll('div.ct').length).toBe(initialContainers)
|
|
282
|
-
expect(el.mountCount).toBe(2)
|
|
283
|
-
expect(el.lastIsRemount).toBe(true)
|
|
284
|
-
|
|
285
|
-
// Second detach + reattach cycle — counts still stable.
|
|
286
|
-
el.remove()
|
|
287
|
-
document.body.appendChild(el)
|
|
288
|
-
|
|
289
|
-
expect(shadow.childElementCount).toBe(initialChildCount)
|
|
290
|
-
expect(shadow.querySelectorAll('style').length).toBe(initialStyles)
|
|
291
|
-
expect(shadow.querySelectorAll('div.ct').length).toBe(initialContainers)
|
|
292
|
-
expect(el.mountCount).toBe(3)
|
|
293
|
-
expect(el.lastIsRemount).toBe(true)
|
|
294
|
-
|
|
295
|
-
el.remove()
|
|
296
|
-
})
|
|
297
|
-
|
|
298
|
-
it('reuses the same container instance on reconnect', () => {
|
|
299
|
-
const el = document.createElement(cssTag) as CssEl
|
|
300
|
-
document.body.appendChild(el)
|
|
301
|
-
const firstContainer = el.lastContainer
|
|
302
|
-
expect(firstContainer).not.toBeNull()
|
|
303
|
-
|
|
304
|
-
el.remove()
|
|
305
|
-
document.body.appendChild(el)
|
|
306
|
-
|
|
307
|
-
expect(el.lastContainer).toBe(firstContainer)
|
|
308
|
-
el.remove()
|
|
309
|
-
})
|
|
310
|
-
})
|