@pyreon/runtime-dom 0.24.5 → 0.24.6
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/package.json +5 -9
- package/src/delegate.ts +0 -98
- package/src/devtools.ts +0 -339
- package/src/env.d.ts +0 -6
- package/src/hydrate.ts +0 -450
- package/src/hydration-debug.ts +0 -129
- package/src/index.ts +0 -83
- package/src/keep-alive-entry.ts +0 -3
- package/src/keep-alive.ts +0 -83
- package/src/manifest.ts +0 -236
- package/src/mount.ts +0 -597
- package/src/nodes.ts +0 -896
- package/src/props.ts +0 -474
- package/src/template.ts +0 -523
- package/src/tests/callback-ref-unmount.browser.test.ts +0 -62
- package/src/tests/callback-ref-unmount.test.ts +0 -52
- package/src/tests/compiler-integration.test.tsx +0 -508
- package/src/tests/coverage-gaps.test.ts +0 -3183
- package/src/tests/coverage.test.ts +0 -1140
- package/src/tests/ctx-stack-growth-repro.test.tsx +0 -158
- package/src/tests/dev-gate-pattern.test.ts +0 -46
- package/src/tests/dev-gate-treeshake.test.ts +0 -256
- package/src/tests/error-boundary-stack-leak-repro.test.tsx +0 -133
- package/src/tests/fanout-repro.test.tsx +0 -219
- package/src/tests/hydration-integration.test.tsx +0 -540
- package/src/tests/keyed-array-in-for-batched-toggle.browser.test.ts +0 -140
- package/src/tests/lifecycle-integration.test.tsx +0 -342
- package/src/tests/lis-prepend.browser.test.ts +0 -99
- package/src/tests/manifest-snapshot.test.ts +0 -85
- package/src/tests/mount.test.ts +0 -3529
- package/src/tests/native-markers.test.ts +0 -19
- package/src/tests/props.test.ts +0 -581
- package/src/tests/reactive-props.test.ts +0 -270
- package/src/tests/real-world-integration.test.tsx +0 -714
- package/src/tests/rs-collapse-dyn-h.browser.test.ts +0 -303
- package/src/tests/rs-collapse-dyn.browser.test.ts +0 -316
- package/src/tests/rs-collapse-h.browser.test.ts +0 -152
- package/src/tests/rs-collapse-h.test.ts +0 -237
- package/src/tests/rs-collapse.browser.test.ts +0 -128
- package/src/tests/runtime-dom.browser.test.ts +0 -409
- package/src/tests/setup.ts +0 -3
- package/src/tests/show-context.test.ts +0 -270
- package/src/tests/show-of-for-batched-toggle.browser.test.ts +0 -122
- package/src/tests/ssr-xss-round-trip.browser.test.ts +0 -93
- package/src/tests/style-key-removal.browser.test.ts +0 -54
- package/src/tests/style-key-removal.test.ts +0 -88
- package/src/tests/template.test.ts +0 -383
- package/src/tests/transition-timeout-leak.test.ts +0 -126
- package/src/tests/transition.test.ts +0 -568
- package/src/tests/verified-correct-probes.test.ts +0 -56
- package/src/transition-entry.ts +0 -7
- package/src/transition-group.ts +0 -350
- package/src/transition.ts +0 -245
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
import { signal } from '@pyreon/reactivity'
|
|
2
|
-
import { flush } from '@pyreon/test-utils/browser'
|
|
3
|
-
import { afterEach, describe, expect, it } from 'vitest'
|
|
4
|
-
import { _rsCollapseH, mount } from '../index'
|
|
5
|
-
|
|
6
|
-
// PR 2 of the partial-collapse build (open-work #1), in REAL Chromium.
|
|
7
|
-
// `_rsCollapseH` = `_rsCollapse` (one _tpl cloneNode, dual-emit reactive
|
|
8
|
-
// class, no remount) PLUS it re-attaches the residual `on*` handlers the
|
|
9
|
-
// compiler's `detectPartialCollapsibleShape` (PR 1) peeled off. The ONLY
|
|
10
|
-
// delta vs `_rsCollapse` is the handler re-attach, routed through the
|
|
11
|
-
// canonical `_bindEvent` → `applyEventProp` path — so a partially-
|
|
12
|
-
// collapsed `<Button onClick=…>` behaves byte-identically to the
|
|
13
|
-
// 5-layer mount it replaced. These specs prove exactly that delta in a
|
|
14
|
-
// real browser (real click/pointer events, real computed style).
|
|
15
|
-
//
|
|
16
|
-
// Bisect-verify (PR body): neutralize the handler loop in
|
|
17
|
-
// `_rsCollapseH` (`for (const key in handlers)` → no-op) → the 3
|
|
18
|
-
// handler specs fail (`expected 0 to be 1` — handler never fired) while
|
|
19
|
-
// every class/mode/no-remount assertion still passes; restore → all
|
|
20
|
-
// pass. That asymmetry proves the handler re-attach is the load-bearing
|
|
21
|
-
// addition, not passing for the wrong reason.
|
|
22
|
-
|
|
23
|
-
describe('_rsCollapseH (real browser) — PR 2 partial-collapse runtime', () => {
|
|
24
|
-
const cleanup: Array<() => void> = []
|
|
25
|
-
afterEach(() => {
|
|
26
|
-
for (const u of cleanup.splice(0)) u()
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
function injectCss(css: string): void {
|
|
30
|
-
const el = document.createElement('style')
|
|
31
|
-
el.textContent = css
|
|
32
|
-
document.head.appendChild(el)
|
|
33
|
-
cleanup.push(() => el.remove())
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function mountInto(node: ReturnType<typeof _rsCollapseH>): HTMLElement {
|
|
37
|
-
const root = document.createElement('div')
|
|
38
|
-
document.body.appendChild(root)
|
|
39
|
-
const dispose = mount(node as unknown as Parameters<typeof mount>[0], root)
|
|
40
|
-
cleanup.push(() => {
|
|
41
|
-
dispose()
|
|
42
|
-
root.remove()
|
|
43
|
-
})
|
|
44
|
-
return root
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
it('applies the light class AND fires the peeled onClick on a real click', async () => {
|
|
48
|
-
injectCss('.rsh-l{color:rgb(1,2,3)}.rsh-d{color:rgb(9,8,7)}')
|
|
49
|
-
const isDark = signal(false)
|
|
50
|
-
let clicks = 0
|
|
51
|
-
const root = mountInto(
|
|
52
|
-
_rsCollapseH('<button>Save</button>', 'rsh-l', 'rsh-d', () => isDark(), {
|
|
53
|
-
onClick: () => {
|
|
54
|
-
clicks++
|
|
55
|
-
},
|
|
56
|
-
}),
|
|
57
|
-
)
|
|
58
|
-
await flush()
|
|
59
|
-
const btn = root.querySelector('button') as HTMLButtonElement
|
|
60
|
-
expect(btn).not.toBeNull()
|
|
61
|
-
expect(btn.className).toBe('rsh-l')
|
|
62
|
-
expect(btn.textContent).toBe('Save')
|
|
63
|
-
expect(getComputedStyle(btn).color).toBe('rgb(1, 2, 3)')
|
|
64
|
-
|
|
65
|
-
btn.click()
|
|
66
|
-
expect(clicks).toBe(1)
|
|
67
|
-
btn.click()
|
|
68
|
-
expect(clicks).toBe(2)
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
it('mode flip swaps the class on the SAME node AND the handler survives the flip', async () => {
|
|
72
|
-
injectCss('.rsh-l2{color:rgb(10,20,30)}.rsh-d2{color:rgb(40,50,60)}')
|
|
73
|
-
const isDark = signal(false)
|
|
74
|
-
let clicks = 0
|
|
75
|
-
const root = mountInto(
|
|
76
|
-
_rsCollapseH('<button>X</button>', 'rsh-l2', 'rsh-d2', () => isDark(), {
|
|
77
|
-
onClick: () => {
|
|
78
|
-
clicks++
|
|
79
|
-
},
|
|
80
|
-
}),
|
|
81
|
-
)
|
|
82
|
-
await flush()
|
|
83
|
-
const before = root.querySelector('button') as HTMLButtonElement
|
|
84
|
-
expect(before.className).toBe('rsh-l2')
|
|
85
|
-
before.click()
|
|
86
|
-
expect(clicks).toBe(1)
|
|
87
|
-
|
|
88
|
-
isDark.set(true)
|
|
89
|
-
await flush()
|
|
90
|
-
const after = root.querySelector('button') as HTMLButtonElement
|
|
91
|
-
expect(after).toBe(before) // node identity preserved ⇒ reactive, not remount
|
|
92
|
-
expect(after.className).toBe('rsh-d2')
|
|
93
|
-
// The load-bearing partial-collapse contract: the reactive class
|
|
94
|
-
// binding does NOT remount, so the handler attached at first mount is
|
|
95
|
-
// still live after the mode flip.
|
|
96
|
-
after.click()
|
|
97
|
-
expect(clicks).toBe(2)
|
|
98
|
-
})
|
|
99
|
-
|
|
100
|
-
it('peels + binds MULTIPLE handlers with correct onXxx→event normalization', async () => {
|
|
101
|
-
injectCss('.rsh-m{color:rgb(0,0,0)}')
|
|
102
|
-
const isDark = signal(false)
|
|
103
|
-
let clicks = 0
|
|
104
|
-
let enters = 0
|
|
105
|
-
const root = mountInto(
|
|
106
|
-
_rsCollapseH('<button>M</button>', 'rsh-m', 'rsh-m', () => isDark(), {
|
|
107
|
-
onClick: () => {
|
|
108
|
-
clicks++
|
|
109
|
-
},
|
|
110
|
-
// onPointerEnter must normalize to the lowercase DOM event
|
|
111
|
-
// `pointerenter` via the canonical path — a hand-rolled
|
|
112
|
-
// `addEventListener('pointerEnter', …)` would never fire.
|
|
113
|
-
onPointerEnter: () => {
|
|
114
|
-
enters++
|
|
115
|
-
},
|
|
116
|
-
}),
|
|
117
|
-
)
|
|
118
|
-
await flush()
|
|
119
|
-
const btn = root.querySelector('button') as HTMLButtonElement
|
|
120
|
-
btn.click()
|
|
121
|
-
btn.dispatchEvent(new PointerEvent('pointerenter', { bubbles: false }))
|
|
122
|
-
expect(clicks).toBe(1)
|
|
123
|
-
expect(enters).toBe(1)
|
|
124
|
-
})
|
|
125
|
-
|
|
126
|
-
it('dispose removes the listener (no leak) — composed cleanup is correct', async () => {
|
|
127
|
-
injectCss('.rsh-c{color:rgb(5,5,5)}')
|
|
128
|
-
const isDark = signal(false)
|
|
129
|
-
let clicks = 0
|
|
130
|
-
const root = document.createElement('div')
|
|
131
|
-
document.body.appendChild(root)
|
|
132
|
-
const dispose = mount(
|
|
133
|
-
_rsCollapseH('<button>C</button>', 'rsh-c', 'rsh-c', () => isDark(), {
|
|
134
|
-
onClick: () => {
|
|
135
|
-
clicks++
|
|
136
|
-
},
|
|
137
|
-
}) as unknown as Parameters<typeof mount>[0],
|
|
138
|
-
root,
|
|
139
|
-
)
|
|
140
|
-
await flush()
|
|
141
|
-
const btn = root.querySelector('button') as HTMLButtonElement
|
|
142
|
-
btn.click()
|
|
143
|
-
expect(clicks).toBe(1)
|
|
144
|
-
|
|
145
|
-
dispose()
|
|
146
|
-
// After dispose the handler disposer ran — the listener is gone, so a
|
|
147
|
-
// post-dispose click does NOT increment.
|
|
148
|
-
btn.click()
|
|
149
|
-
expect(clicks).toBe(1)
|
|
150
|
-
root.remove()
|
|
151
|
-
})
|
|
152
|
-
})
|
|
@@ -1,237 +0,0 @@
|
|
|
1
|
-
import { signal } from '@pyreon/reactivity'
|
|
2
|
-
import { afterEach, describe, expect, it } from 'vitest'
|
|
3
|
-
import { _rsCollapseH } from '../template'
|
|
4
|
-
|
|
5
|
-
// PR 2 of the partial-collapse build (open-work #1) — happy-dom UNIT
|
|
6
|
-
// layer (fast + locally bisect-verifiable). Imports ONLY `../template`
|
|
7
|
-
// (template.test.ts style) — NOT `../index`, whose wide cross-package
|
|
8
|
-
// re-export graph hits the documented fresh-worktree resolution trap.
|
|
9
|
-
//
|
|
10
|
-
// Event-path split (honest, per test-environment-parity — both layers):
|
|
11
|
-
// - DELEGATED events (`click`, see `delegate.ts:DELEGATED_EVENTS`)
|
|
12
|
-
// park in a prop-slot that only fires via the root listener
|
|
13
|
-
// `mount()` installs → covered by `rs-collapse-h.browser.test.ts`
|
|
14
|
-
// (real Chromium + real `mount()`, CI-authoritative; can't run in a
|
|
15
|
-
// fresh worktree — `@pyreon/test-utils/browser` needs built lib).
|
|
16
|
-
// - NON-delegated events (`pointerenter`, `mouseenter` — NOT in
|
|
17
|
-
// `DELEGATED_EVENTS`) take `applyEventProp`'s direct
|
|
18
|
-
// `el.addEventListener(name, …)` path → fire on a bare
|
|
19
|
-
// `dispatchEvent` with NO `mount()`. This unit drives the
|
|
20
|
-
// handler-attach delta on THAT path: it still proves `_rsCollapseH`
|
|
21
|
-
// routes residual handlers through the canonical
|
|
22
|
-
// `_bindEvent`→`applyEventProp` path (incl. the exact
|
|
23
|
-
// `onXxx`→lowercase normalization) + composed cleanup — the
|
|
24
|
-
// load-bearing addition vs `_rsCollapse`.
|
|
25
|
-
//
|
|
26
|
-
// `_rsCollapseH` returns a NativeItem (`{ __isNative, el, cleanup }`);
|
|
27
|
-
// `_tpl` runs the bind synchronously at call time, so class + handlers
|
|
28
|
-
// are live on `.el` immediately (same direct-NativeItem shape
|
|
29
|
-
// `template.test.ts` uses for `_tpl`). Signal writes propagate
|
|
30
|
-
// synchronously here (cf. `template.test.ts:_bindText`); a
|
|
31
|
-
// `Promise.resolve()` tick covers any microtask-scheduled reactive
|
|
32
|
-
// class update defensively.
|
|
33
|
-
//
|
|
34
|
-
// Bisect-verify (PR body): neutralize the handler loop in
|
|
35
|
-
// `_rsCollapseH` (`for (const key in handlers)` body → skip) → the
|
|
36
|
-
// handler/normalization/cleanup specs fail with `expected 0 to be 1`
|
|
37
|
-
// while every class / mode-flip / no-remount / zero-handler / child
|
|
38
|
-
// assertion still passes; restore → 8/8. The asymmetry proves the
|
|
39
|
-
// handler re-attach is the load-bearing delta.
|
|
40
|
-
|
|
41
|
-
const tick = (): Promise<void> => Promise.resolve()
|
|
42
|
-
|
|
43
|
-
describe('_rsCollapseH (happy-dom unit) — PR 2 partial-collapse runtime', () => {
|
|
44
|
-
const cleanup: Array<() => void> = []
|
|
45
|
-
afterEach(() => {
|
|
46
|
-
for (const u of cleanup.splice(0)) u()
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
function place(item: ReturnType<typeof _rsCollapseH>): HTMLElement {
|
|
50
|
-
const el = item.el as HTMLElement
|
|
51
|
-
document.body.appendChild(el)
|
|
52
|
-
cleanup.push(() => {
|
|
53
|
-
item.cleanup?.()
|
|
54
|
-
el.remove()
|
|
55
|
-
})
|
|
56
|
-
return el
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const fire = (el: HTMLElement, type: string): void => {
|
|
60
|
-
el.dispatchEvent(new Event(type, { bubbles: false }))
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
it('sets the light class + static children AND fires the peeled handler', () => {
|
|
64
|
-
const isDark = signal(false)
|
|
65
|
-
let enters = 0
|
|
66
|
-
const el = place(
|
|
67
|
-
_rsCollapseH('<button>Save</button>', 'rsh-l', 'rsh-d', () => isDark(), {
|
|
68
|
-
onPointerEnter: () => {
|
|
69
|
-
enters++
|
|
70
|
-
},
|
|
71
|
-
}),
|
|
72
|
-
)
|
|
73
|
-
expect(el.tagName).toBe('BUTTON')
|
|
74
|
-
expect(el.className).toBe('rsh-l')
|
|
75
|
-
expect(el.textContent).toBe('Save')
|
|
76
|
-
|
|
77
|
-
fire(el, 'pointerenter')
|
|
78
|
-
expect(enters).toBe(1)
|
|
79
|
-
fire(el, 'pointerenter')
|
|
80
|
-
expect(enters).toBe(2)
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
it('normalizes onXxx → lowercase DOM event via the canonical path', () => {
|
|
84
|
-
const isDark = signal(false)
|
|
85
|
-
let enters = 0
|
|
86
|
-
let mouse = 0
|
|
87
|
-
const el = place(
|
|
88
|
-
_rsCollapseH('<button>M</button>', 'rsh-m', 'rsh-m', () => isDark(), {
|
|
89
|
-
// onPointerEnter MUST bind to `pointerenter` (lowercased whole
|
|
90
|
-
// name) — a hand-rolled `addEventListener('pointerEnter', …)`
|
|
91
|
-
// would never fire. onMouseEnter → `mouseenter` likewise.
|
|
92
|
-
onPointerEnter: () => {
|
|
93
|
-
enters++
|
|
94
|
-
},
|
|
95
|
-
onMouseEnter: () => {
|
|
96
|
-
mouse++
|
|
97
|
-
},
|
|
98
|
-
}),
|
|
99
|
-
)
|
|
100
|
-
fire(el, 'pointerenter')
|
|
101
|
-
fire(el, 'mouseenter')
|
|
102
|
-
expect(enters).toBe(1)
|
|
103
|
-
expect(mouse).toBe(1)
|
|
104
|
-
// Wrong-case names must NOT fire (proves real normalization, not luck).
|
|
105
|
-
fire(el, 'pointerEnter')
|
|
106
|
-
fire(el, 'mouseEnter')
|
|
107
|
-
expect(enters).toBe(1)
|
|
108
|
-
expect(mouse).toBe(1)
|
|
109
|
-
})
|
|
110
|
-
|
|
111
|
-
it('mode flip swaps the class on the SAME node AND the handler survives the flip', async () => {
|
|
112
|
-
const isDark = signal(false)
|
|
113
|
-
let enters = 0
|
|
114
|
-
const el = place(
|
|
115
|
-
_rsCollapseH('<button>X</button>', 'rsh-l2', 'rsh-d2', () => isDark(), {
|
|
116
|
-
onPointerEnter: () => {
|
|
117
|
-
enters++
|
|
118
|
-
},
|
|
119
|
-
}),
|
|
120
|
-
)
|
|
121
|
-
expect(el.className).toBe('rsh-l2')
|
|
122
|
-
fire(el, 'pointerenter')
|
|
123
|
-
expect(enters).toBe(1)
|
|
124
|
-
|
|
125
|
-
isDark.set(true)
|
|
126
|
-
await tick()
|
|
127
|
-
// Same node identity ⇒ reactive class swap, NOT a remount.
|
|
128
|
-
expect(el.className).toBe('rsh-d2')
|
|
129
|
-
// Load-bearing partial-collapse contract: no remount ⇒ the handler
|
|
130
|
-
// bound at construction is still live after the mode flip.
|
|
131
|
-
fire(el, 'pointerenter')
|
|
132
|
-
expect(enters).toBe(2)
|
|
133
|
-
|
|
134
|
-
isDark.set(false)
|
|
135
|
-
await tick()
|
|
136
|
-
expect(el.className).toBe('rsh-l2')
|
|
137
|
-
})
|
|
138
|
-
|
|
139
|
-
it('cleanup() removes the listener — composed disposer is correct (no leak)', () => {
|
|
140
|
-
const isDark = signal(false)
|
|
141
|
-
let enters = 0
|
|
142
|
-
const item = _rsCollapseH('<button>C</button>', 'rsh-c', 'rsh-c', () => isDark(), {
|
|
143
|
-
onPointerEnter: () => {
|
|
144
|
-
enters++
|
|
145
|
-
},
|
|
146
|
-
})
|
|
147
|
-
const el = item.el as HTMLElement
|
|
148
|
-
document.body.appendChild(el)
|
|
149
|
-
fire(el, 'pointerenter')
|
|
150
|
-
expect(enters).toBe(1)
|
|
151
|
-
|
|
152
|
-
item.cleanup?.()
|
|
153
|
-
fire(el, 'pointerenter') // listener removed by the composed disposer
|
|
154
|
-
expect(enters).toBe(1)
|
|
155
|
-
el.remove()
|
|
156
|
-
})
|
|
157
|
-
|
|
158
|
-
it('binds MULTIPLE handlers; literal props stay out of the handler set', () => {
|
|
159
|
-
const isDark = signal(false)
|
|
160
|
-
let a = 0
|
|
161
|
-
let b = 0
|
|
162
|
-
const el = place(
|
|
163
|
-
_rsCollapseH('<button>N</button>', 'rsh-n', 'rsh-n', () => isDark(), {
|
|
164
|
-
onPointerEnter: () => {
|
|
165
|
-
a++
|
|
166
|
-
},
|
|
167
|
-
onMouseEnter: () => {
|
|
168
|
-
b++
|
|
169
|
-
},
|
|
170
|
-
}),
|
|
171
|
-
)
|
|
172
|
-
fire(el, 'pointerenter')
|
|
173
|
-
fire(el, 'mouseenter')
|
|
174
|
-
fire(el, 'pointerenter')
|
|
175
|
-
expect(a).toBe(2)
|
|
176
|
-
expect(b).toBe(1)
|
|
177
|
-
})
|
|
178
|
-
|
|
179
|
-
it('zero handlers: renders class + children, cleanup is safe (defensive)', async () => {
|
|
180
|
-
const isDark = signal(false)
|
|
181
|
-
const el = place(_rsCollapseH('<button>Z</button>', 'rsh-z', 'rsh-zd', () => isDark(), {}))
|
|
182
|
-
expect(el.className).toBe('rsh-z')
|
|
183
|
-
expect(el.textContent).toBe('Z')
|
|
184
|
-
isDark.set(true)
|
|
185
|
-
await tick()
|
|
186
|
-
expect(el.className).toBe('rsh-zd')
|
|
187
|
-
// cleanup runs via afterEach — must not throw with no handlers.
|
|
188
|
-
})
|
|
189
|
-
|
|
190
|
-
it('children bind runs alongside the class + handler binds', () => {
|
|
191
|
-
const isDark = signal(false)
|
|
192
|
-
let enters = 0
|
|
193
|
-
const el = place(
|
|
194
|
-
_rsCollapseH(
|
|
195
|
-
'<button><span></span></button>',
|
|
196
|
-
'rsh-cb',
|
|
197
|
-
'rsh-cbd',
|
|
198
|
-
() => isDark(),
|
|
199
|
-
{ onPointerEnter: () => enters++ },
|
|
200
|
-
(root) => {
|
|
201
|
-
;(root.querySelector('span') as HTMLElement).textContent = 'child'
|
|
202
|
-
return null
|
|
203
|
-
},
|
|
204
|
-
),
|
|
205
|
-
)
|
|
206
|
-
expect(el.className).toBe('rsh-cb')
|
|
207
|
-
expect((el.querySelector('span') as HTMLElement).textContent).toBe('child')
|
|
208
|
-
fire(el, 'pointerenter')
|
|
209
|
-
expect(enters).toBe(1)
|
|
210
|
-
})
|
|
211
|
-
|
|
212
|
-
it('disposes class + handler + child binds together (composition)', () => {
|
|
213
|
-
const isDark = signal(false)
|
|
214
|
-
let enters = 0
|
|
215
|
-
let childDisposed = false
|
|
216
|
-
const item = _rsCollapseH(
|
|
217
|
-
'<button><span></span></button>',
|
|
218
|
-
'rsh-x',
|
|
219
|
-
'rsh-xd',
|
|
220
|
-
() => isDark(),
|
|
221
|
-
{ onPointerEnter: () => enters++ },
|
|
222
|
-
() => () => {
|
|
223
|
-
childDisposed = true
|
|
224
|
-
},
|
|
225
|
-
)
|
|
226
|
-
const el = item.el as HTMLElement
|
|
227
|
-
document.body.appendChild(el)
|
|
228
|
-
fire(el, 'pointerenter')
|
|
229
|
-
expect(enters).toBe(1)
|
|
230
|
-
|
|
231
|
-
item.cleanup?.()
|
|
232
|
-
expect(childDisposed).toBe(true) // child disposer composed + ran
|
|
233
|
-
fire(el, 'pointerenter')
|
|
234
|
-
expect(enters).toBe(1) // handler disposer ran too
|
|
235
|
-
el.remove()
|
|
236
|
-
})
|
|
237
|
-
})
|
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
import { signal } from '@pyreon/reactivity'
|
|
2
|
-
import { flush } from '@pyreon/test-utils/browser'
|
|
3
|
-
import { afterEach, describe, expect, it } from 'vitest'
|
|
4
|
-
import { _rsCollapse, mount } from '../index'
|
|
5
|
-
|
|
6
|
-
// Layer 2 of the P0 rocketstyle-collapse slice, in real Chromium.
|
|
7
|
-
// `_rsCollapse` deliberately does NOT import @pyreon/styler (layer
|
|
8
|
-
// purity — runtime-dom is layer 4; the styler injection is the EMITTED
|
|
9
|
-
// code's job). So this suite injects CSS via a plain <style> and proves
|
|
10
|
-
// only what _rsCollapse owns: ONE _tpl() cloneNode whose class is
|
|
11
|
-
// reactively bound to the app mode (dual-emit, RFC decision 1) — a mode
|
|
12
|
-
// flip swaps the class on the SAME node with no remount.
|
|
13
|
-
|
|
14
|
-
describe('_rsCollapse (real browser)', () => {
|
|
15
|
-
const cleanup: Array<() => void> = []
|
|
16
|
-
afterEach(() => {
|
|
17
|
-
for (const u of cleanup.splice(0)) u()
|
|
18
|
-
})
|
|
19
|
-
|
|
20
|
-
function injectCss(css: string): void {
|
|
21
|
-
const el = document.createElement('style')
|
|
22
|
-
el.textContent = css
|
|
23
|
-
document.head.appendChild(el)
|
|
24
|
-
cleanup.push(() => el.remove())
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function mountInto(node: ReturnType<typeof _rsCollapse>): HTMLElement {
|
|
28
|
-
const root = document.createElement('div')
|
|
29
|
-
document.body.appendChild(root)
|
|
30
|
-
const dispose = mount(node as unknown as Parameters<typeof mount>[0], root)
|
|
31
|
-
cleanup.push(() => {
|
|
32
|
-
dispose()
|
|
33
|
-
root.remove()
|
|
34
|
-
})
|
|
35
|
-
return root
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
it('applies the light class + static children; the class is real CSS', async () => {
|
|
39
|
-
injectCss('.rsc-light{color:rgb(1,2,3)}.rsc-dark{color:rgb(9,8,7)}')
|
|
40
|
-
const isDark = signal(false)
|
|
41
|
-
const root = mountInto(
|
|
42
|
-
_rsCollapse('<button>Save</button>', 'rsc-light', 'rsc-dark', () => isDark()),
|
|
43
|
-
)
|
|
44
|
-
await flush()
|
|
45
|
-
const btn = root.querySelector('button')
|
|
46
|
-
expect(btn).not.toBeNull()
|
|
47
|
-
expect(btn?.className).toBe('rsc-light')
|
|
48
|
-
expect(btn?.textContent).toBe('Save')
|
|
49
|
-
expect(getComputedStyle(btn as Element).color).toBe('rgb(1, 2, 3)')
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
it('mode flip swaps to the dark class on the SAME node (no remount)', async () => {
|
|
53
|
-
injectCss('.rsc-l2{color:rgb(10,20,30)}.rsc-d2{color:rgb(40,50,60)}')
|
|
54
|
-
const isDark = signal(false)
|
|
55
|
-
const root = mountInto(
|
|
56
|
-
_rsCollapse('<button>X</button>', 'rsc-l2', 'rsc-d2', () => isDark()),
|
|
57
|
-
)
|
|
58
|
-
await flush()
|
|
59
|
-
const before = root.querySelector('button') as HTMLElement
|
|
60
|
-
expect(before.className).toBe('rsc-l2')
|
|
61
|
-
|
|
62
|
-
isDark.set(true)
|
|
63
|
-
await flush()
|
|
64
|
-
const after = root.querySelector('button') as HTMLElement
|
|
65
|
-
expect(after).toBe(before) // node identity preserved ⇒ reactive, not remount
|
|
66
|
-
expect(after.className).toBe('rsc-d2')
|
|
67
|
-
expect(getComputedStyle(after).color).toBe('rgb(40, 50, 60)')
|
|
68
|
-
|
|
69
|
-
isDark.set(false)
|
|
70
|
-
await flush()
|
|
71
|
-
expect((root.querySelector('button') as HTMLElement).className).toBe('rsc-l2')
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
it('children bind runs alongside the class bind and disposes cleanly', async () => {
|
|
75
|
-
injectCss('.rsc-c{color:rgb(2,2,2)}.rsc-cd{color:rgb(3,3,3)}')
|
|
76
|
-
const label = signal('one')
|
|
77
|
-
const isDark = signal(false)
|
|
78
|
-
let childDisposed = false
|
|
79
|
-
const root = mountInto(
|
|
80
|
-
_rsCollapse(
|
|
81
|
-
'<button><span></span></button>',
|
|
82
|
-
'rsc-c',
|
|
83
|
-
'rsc-cd',
|
|
84
|
-
() => isDark(),
|
|
85
|
-
(el) => {
|
|
86
|
-
const span = el.querySelector('span') as HTMLElement
|
|
87
|
-
const stop = (() => {
|
|
88
|
-
// minimal reactive child without pulling the compiler in
|
|
89
|
-
let raf = 0
|
|
90
|
-
const tick = () => {
|
|
91
|
-
span.textContent = label()
|
|
92
|
-
raf = requestAnimationFrame(tick)
|
|
93
|
-
}
|
|
94
|
-
tick()
|
|
95
|
-
return () => {
|
|
96
|
-
cancelAnimationFrame(raf)
|
|
97
|
-
childDisposed = true
|
|
98
|
-
}
|
|
99
|
-
})()
|
|
100
|
-
return stop
|
|
101
|
-
},
|
|
102
|
-
),
|
|
103
|
-
)
|
|
104
|
-
await flush()
|
|
105
|
-
expect((root.querySelector('span') as HTMLElement).textContent).toBe('one')
|
|
106
|
-
expect((root.querySelector('button') as HTMLElement).className).toBe('rsc-c')
|
|
107
|
-
// dispose via afterEach → child cleanup must fire
|
|
108
|
-
for (const u of cleanup.splice(0)) u()
|
|
109
|
-
expect(childDisposed).toBe(true)
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
it('two instances of the same html share ONE parsed template, with independent reactivity', async () => {
|
|
113
|
-
injectCss('.rsc-s{color:rgb(7,7,7)}.rsc-sd{color:rgb(8,8,8)}')
|
|
114
|
-
const isDark = signal(false)
|
|
115
|
-
const r1 = mountInto(_rsCollapse('<button>dup</button>', 'rsc-s', 'rsc-sd', () => isDark()))
|
|
116
|
-
const r2 = mountInto(_rsCollapse('<button>dup</button>', 'rsc-s', 'rsc-sd', () => isDark()))
|
|
117
|
-
await flush()
|
|
118
|
-
const b1 = r1.querySelector('button') as HTMLElement
|
|
119
|
-
const b2 = r2.querySelector('button') as HTMLElement
|
|
120
|
-
expect(b1).not.toBe(b2) // distinct cloned nodes from the shared template
|
|
121
|
-
expect(b1.className).toBe('rsc-s')
|
|
122
|
-
expect(b2.className).toBe('rsc-s')
|
|
123
|
-
isDark.set(true)
|
|
124
|
-
await flush()
|
|
125
|
-
expect((r1.querySelector('button') as HTMLElement).className).toBe('rsc-sd')
|
|
126
|
-
expect((r2.querySelector('button') as HTMLElement).className).toBe('rsc-sd')
|
|
127
|
-
})
|
|
128
|
-
})
|