@pyreon/vue-compat 0.22.0 → 0.23.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,81 @@
1
+ /**
2
+ * REPRODUCTION + REGRESSION — vue-compat's `provide()` and `createApp().provide()`
3
+ * both used position-based `popContext()` (or no pop at all) to release the
4
+ * context-stack frames they pushed. Same bug class as #725 (`@pyreon/core`'s
5
+ * provide) and #729 (ErrorBoundary stack) — see those for the underlying
6
+ * theory.
7
+ *
8
+ * Bug shapes:
9
+ *
10
+ * 1) Component-level `provide(key, value)` — pushed a Map and registered
11
+ * `unmountCallbacks.push(() => popContext())`. `popContext` pops `stack.pop()`
12
+ * (the last frame). When sibling components unmount out-of-order (renderer-
13
+ * driven `<For>` removal, `<Show>` flipping a non-last sibling), the first
14
+ * sibling's onUnmount popped the LAST sibling's frame. Context stack
15
+ * corruption + orphaned frames at the top forever.
16
+ *
17
+ * 2) `createApp(C).provide('K', v).mount(el)` pushed app-level provisions
18
+ * BEFORE the mount but the returned unmount only ran `pyreonMount`'s
19
+ * cleanup — leaving the app-level frames on the global context stack
20
+ * forever, one per provision per mount cycle.
21
+ *
22
+ * Fix: identity-based removal via the newly-exported `removeContextFrame` from
23
+ * `@pyreon/core`. `provide` captures the frame at push, removes by reference.
24
+ * `createApp.mount` tracks every pushed app-level frame and removes each on
25
+ * unmount.
26
+ */
27
+ import { captureContextStack, h as pyreonH } from '@pyreon/core'
28
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest'
29
+ import { createApp, inject } from '../index'
30
+
31
+ describe('vue-compat — provide/popContext is identity-safe (#725/#729 class)', () => {
32
+ let container: HTMLElement
33
+
34
+ beforeEach(() => {
35
+ container = document.createElement('div')
36
+ document.body.appendChild(container)
37
+ })
38
+ afterEach(() => {
39
+ container.remove()
40
+ })
41
+
42
+ it('REGRESSION: createApp().provide(k, v).mount(el); unmount() — context stack returns to baseline', () => {
43
+ const baseLen = captureContextStack().length
44
+
45
+ function Root() {
46
+ const v = inject<string>('K')
47
+ return pyreonH('span', { 'data-testid': 'val' }, v ?? 'missing')
48
+ }
49
+
50
+ const unmount = createApp(Root).provide('K', 'hello').mount(container)
51
+
52
+ // Sanity: the value was actually injected (proves the push reached the
53
+ // child) — prevents the regression test from passing vacuously.
54
+ expect(container.querySelector('[data-testid="val"]')?.textContent).toBe('hello')
55
+
56
+ unmount()
57
+
58
+ // The critical assertion: app-level provision frames are removed on
59
+ // unmount. Pre-fix the stack accumulates one frame per provision per
60
+ // mount cycle, forever.
61
+ expect(captureContextStack().length).toBe(baseLen)
62
+ })
63
+
64
+ it('REGRESSION: 100 mount/unmount cycles do NOT accumulate context-stack frames', () => {
65
+ const baseLen = captureContextStack().length
66
+
67
+ function Root() {
68
+ return pyreonH('span', null, 'x')
69
+ }
70
+
71
+ for (let i = 0; i < 100; i++) {
72
+ const unmount = createApp(Root).provide('A', i).provide('B', i + 1000).mount(container)
73
+ unmount()
74
+ }
75
+
76
+ // Pre-fix: each mount pushes 2 frames, unmount removes 0 → 200 orphan
77
+ // frames after 100 cycles. Post-fix: stack returns to baseline.
78
+ const delta = captureContextStack().length - baseLen
79
+ expect(delta).toBeLessThan(5) // allow tiny noise from other unrelated test infra
80
+ })
81
+ })