@pyreon/core 0.24.4 → 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/lib/analysis/index.js.html +1 -1
- package/lib/index.js +53 -31
- package/package.json +2 -6
- package/src/compat-marker.ts +0 -79
- package/src/compat-shared.ts +0 -80
- package/src/component.ts +0 -98
- package/src/context.ts +0 -349
- package/src/defer.ts +0 -279
- package/src/dynamic.ts +0 -32
- package/src/env.d.ts +0 -6
- package/src/error-boundary.ts +0 -90
- package/src/for.ts +0 -51
- package/src/h.ts +0 -80
- package/src/index.ts +0 -80
- package/src/jsx-dev-runtime.ts +0 -2
- package/src/jsx-runtime.ts +0 -747
- package/src/lazy.ts +0 -25
- package/src/lifecycle.ts +0 -152
- package/src/manifest.ts +0 -579
- package/src/map-array.ts +0 -42
- package/src/portal.ts +0 -39
- package/src/props.ts +0 -269
- package/src/ref.ts +0 -32
- package/src/show.ts +0 -121
- package/src/style.ts +0 -102
- package/src/suspense.ts +0 -52
- package/src/telemetry.ts +0 -120
- package/src/tests/compat-marker.test.ts +0 -96
- package/src/tests/compat-shared.test.ts +0 -99
- package/src/tests/component.test.ts +0 -281
- package/src/tests/context.test.ts +0 -629
- package/src/tests/core.test.ts +0 -1290
- package/src/tests/cx.test.ts +0 -70
- package/src/tests/defer.test.ts +0 -359
- package/src/tests/dynamic.test.ts +0 -87
- package/src/tests/error-boundary.test.ts +0 -181
- package/src/tests/extract-props-overloads.types.test.ts +0 -135
- package/src/tests/for.test.ts +0 -117
- package/src/tests/h.test.ts +0 -221
- package/src/tests/jsx-compat.test.tsx +0 -86
- package/src/tests/lazy.test.ts +0 -100
- package/src/tests/lifecycle.test.ts +0 -350
- package/src/tests/manifest-snapshot.test.ts +0 -100
- package/src/tests/map-array.test.ts +0 -313
- package/src/tests/native-marker-error-boundary.test.ts +0 -12
- package/src/tests/portal.test.ts +0 -48
- package/src/tests/props-extended.test.ts +0 -157
- package/src/tests/props.test.ts +0 -250
- package/src/tests/reactive-context.test.ts +0 -69
- package/src/tests/reactive-props.test.ts +0 -157
- package/src/tests/ref.test.ts +0 -70
- package/src/tests/show.test.ts +0 -314
- package/src/tests/style.test.ts +0 -157
- package/src/tests/suspense.test.ts +0 -139
- package/src/tests/telemetry.test.ts +0 -297
- package/src/types.ts +0 -116
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest'
|
|
2
|
-
import { isNativeCompat, NATIVE_COMPAT_MARKER, nativeCompat } from '../compat-marker'
|
|
3
|
-
|
|
4
|
-
describe('NATIVE_COMPAT_MARKER', () => {
|
|
5
|
-
it('is the same registry symbol regardless of how it is referenced', () => {
|
|
6
|
-
// Symbol.for(...) registry contract — every consumer that uses the same
|
|
7
|
-
// string key (compat layers reading it, framework packages writing it)
|
|
8
|
-
// gets the SAME symbol identity. Changing the string is a breaking
|
|
9
|
-
// change to the marker contract.
|
|
10
|
-
expect(NATIVE_COMPAT_MARKER).toBe(Symbol.for('pyreon:native-compat'))
|
|
11
|
-
})
|
|
12
|
-
|
|
13
|
-
it('is a `symbol`-typed value', () => {
|
|
14
|
-
expect(typeof NATIVE_COMPAT_MARKER).toBe('symbol')
|
|
15
|
-
})
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
describe('nativeCompat', () => {
|
|
19
|
-
it('attaches the marker to a function and returns the same reference', () => {
|
|
20
|
-
function RouterView() {
|
|
21
|
-
return null
|
|
22
|
-
}
|
|
23
|
-
const marked = nativeCompat(RouterView)
|
|
24
|
-
expect(marked).toBe(RouterView)
|
|
25
|
-
expect((RouterView as unknown as Record<symbol, boolean>)[NATIVE_COMPAT_MARKER]).toBe(true)
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
it('is idempotent — applying twice yields the same property state', () => {
|
|
29
|
-
const Component = () => null
|
|
30
|
-
nativeCompat(Component)
|
|
31
|
-
nativeCompat(Component)
|
|
32
|
-
expect((Component as unknown as Record<symbol, boolean>)[NATIVE_COMPAT_MARKER]).toBe(true)
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
it('passes non-function values through unchanged', () => {
|
|
36
|
-
// Defensive: callers may pipe variables of unknown shape (e.g. lazy
|
|
37
|
-
// imports that resolve to objects, or null during HMR boundary
|
|
38
|
-
// teardown). The helper must be safe regardless.
|
|
39
|
-
expect(nativeCompat(null as unknown)).toBe(null)
|
|
40
|
-
expect(nativeCompat(undefined as unknown)).toBe(undefined)
|
|
41
|
-
const obj = { foo: 'bar' }
|
|
42
|
-
expect(nativeCompat(obj)).toBe(obj)
|
|
43
|
-
expect((obj as unknown as Record<symbol, boolean>)[NATIVE_COMPAT_MARKER]).toBeUndefined()
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
it('preserves the function signature for typed callers', () => {
|
|
47
|
-
// The generic `T` flows through unchanged so framework component
|
|
48
|
-
// exports keep their typed callable shape after wrapping.
|
|
49
|
-
const Typed = (props: { name: string }): string => `hello ${props.name}`
|
|
50
|
-
const marked: typeof Typed = nativeCompat(Typed)
|
|
51
|
-
expect(marked({ name: 'world' })).toBe('hello world')
|
|
52
|
-
})
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
describe('isNativeCompat', () => {
|
|
56
|
-
it('returns true for a marked function', () => {
|
|
57
|
-
const Comp = nativeCompat(() => null)
|
|
58
|
-
expect(isNativeCompat(Comp)).toBe(true)
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
it('returns false for an unmarked function', () => {
|
|
62
|
-
expect(isNativeCompat(() => null)).toBe(false)
|
|
63
|
-
})
|
|
64
|
-
|
|
65
|
-
it('returns false for non-function inputs', () => {
|
|
66
|
-
expect(isNativeCompat(null)).toBe(false)
|
|
67
|
-
expect(isNativeCompat(undefined)).toBe(false)
|
|
68
|
-
expect(isNativeCompat('string')).toBe(false)
|
|
69
|
-
expect(isNativeCompat(42)).toBe(false)
|
|
70
|
-
expect(isNativeCompat({ [NATIVE_COMPAT_MARKER]: true })).toBe(false)
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
it('returns false when the marker is set to a non-true value', () => {
|
|
74
|
-
// Defensive against accidental shape mismatch — only `=== true` qualifies.
|
|
75
|
-
function Comp() {
|
|
76
|
-
return null
|
|
77
|
-
}
|
|
78
|
-
;(Comp as unknown as Record<symbol, unknown>)[NATIVE_COMPAT_MARKER] = 1
|
|
79
|
-
expect(isNativeCompat(Comp)).toBe(false)
|
|
80
|
-
;(Comp as unknown as Record<symbol, unknown>)[NATIVE_COMPAT_MARKER] = 'yes'
|
|
81
|
-
expect(isNativeCompat(Comp)).toBe(false)
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
it('reads the same registry symbol that nativeCompat writes', () => {
|
|
85
|
-
// Cross-side contract: a function marked here is detectable by
|
|
86
|
-
// someone who looked up the symbol via `Symbol.for('pyreon:native-compat')`
|
|
87
|
-
// independently — without importing NATIVE_COMPAT_MARKER from this module.
|
|
88
|
-
const Comp = nativeCompat(function Comp() {
|
|
89
|
-
return null
|
|
90
|
-
})
|
|
91
|
-
const externallyDiscoveredSymbol = Symbol.for('pyreon:native-compat')
|
|
92
|
-
expect(
|
|
93
|
-
(Comp as unknown as Record<symbol, boolean>)[externallyDiscoveredSymbol],
|
|
94
|
-
).toBe(true)
|
|
95
|
-
})
|
|
96
|
-
})
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest'
|
|
2
|
-
import { mapCompatDomProps, shallowEqualProps } from '../compat-shared'
|
|
3
|
-
|
|
4
|
-
describe('shallowEqualProps', () => {
|
|
5
|
-
it('equal for same-key same-value objects', () => {
|
|
6
|
-
expect(shallowEqualProps({ a: 1, b: 'x' }, { a: 1, b: 'x' })).toBe(true)
|
|
7
|
-
})
|
|
8
|
-
|
|
9
|
-
it('not equal when a value differs', () => {
|
|
10
|
-
expect(shallowEqualProps({ a: 1 }, { a: 2 })).toBe(false)
|
|
11
|
-
})
|
|
12
|
-
|
|
13
|
-
it('not equal when key counts differ', () => {
|
|
14
|
-
expect(shallowEqualProps({ a: 1 }, { a: 1, b: 2 })).toBe(false)
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
it('uses Object.is semantics (NaN equal, ±0 distinct)', () => {
|
|
18
|
-
expect(shallowEqualProps({ n: NaN }, { n: NaN })).toBe(true)
|
|
19
|
-
expect(shallowEqualProps({ z: 0 }, { z: -0 })).toBe(false)
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
it('empty objects are equal', () => {
|
|
23
|
-
expect(shallowEqualProps({}, {})).toBe(true)
|
|
24
|
-
})
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
describe('mapCompatDomProps', () => {
|
|
28
|
-
it('no-op for component (non-string) type', () => {
|
|
29
|
-
const Comp = () => null
|
|
30
|
-
const p: Record<string, unknown> = { className: 'x', htmlFor: 'y' }
|
|
31
|
-
mapCompatDomProps(p, Comp)
|
|
32
|
-
expect(p).toEqual({ className: 'x', htmlFor: 'y' })
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
it('className → class, htmlFor → for', () => {
|
|
36
|
-
const p: Record<string, unknown> = { className: 'btn', htmlFor: 'email' }
|
|
37
|
-
mapCompatDomProps(p, 'label')
|
|
38
|
-
expect(p).toEqual({ class: 'btn', for: 'email' })
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
it('onChange → onInput on input/textarea/select', () => {
|
|
42
|
-
for (const tag of ['input', 'textarea', 'select']) {
|
|
43
|
-
const fn = () => {}
|
|
44
|
-
const p: Record<string, unknown> = { onChange: fn }
|
|
45
|
-
mapCompatDomProps(p, tag)
|
|
46
|
-
expect(p).toEqual({ onInput: fn })
|
|
47
|
-
}
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
it('onChange does not clobber an explicit onInput', () => {
|
|
51
|
-
const onChange = () => {}
|
|
52
|
-
const onInput = () => {}
|
|
53
|
-
const p: Record<string, unknown> = { onChange, onInput }
|
|
54
|
-
mapCompatDomProps(p, 'input')
|
|
55
|
-
expect(p).toEqual({ onInput })
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
it('onChange left alone on non-form elements', () => {
|
|
59
|
-
const onChange = () => {}
|
|
60
|
-
const p: Record<string, unknown> = { onChange }
|
|
61
|
-
mapCompatDomProps(p, 'div')
|
|
62
|
-
expect(p).toEqual({ onChange })
|
|
63
|
-
})
|
|
64
|
-
|
|
65
|
-
it('autoFocus → autofocus', () => {
|
|
66
|
-
const p: Record<string, unknown> = { autoFocus: true }
|
|
67
|
-
mapCompatDomProps(p, 'input')
|
|
68
|
-
expect(p).toEqual({ autofocus: true })
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
it('defaultValue/defaultChecked → value/checked only when uncontrolled', () => {
|
|
72
|
-
const a: Record<string, unknown> = { defaultValue: 'd', defaultChecked: true }
|
|
73
|
-
mapCompatDomProps(a, 'input')
|
|
74
|
-
expect(a).toEqual({ value: 'd', checked: true })
|
|
75
|
-
|
|
76
|
-
const b: Record<string, unknown> = {
|
|
77
|
-
defaultValue: 'd',
|
|
78
|
-
value: 'controlled',
|
|
79
|
-
defaultChecked: true,
|
|
80
|
-
checked: false,
|
|
81
|
-
}
|
|
82
|
-
mapCompatDomProps(b, 'input')
|
|
83
|
-
expect(b).toEqual({
|
|
84
|
-
defaultValue: 'd',
|
|
85
|
-
value: 'controlled',
|
|
86
|
-
defaultChecked: true,
|
|
87
|
-
checked: false,
|
|
88
|
-
})
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
it('strips authoring-only props with no DOM equivalent', () => {
|
|
92
|
-
const p: Record<string, unknown> = {
|
|
93
|
-
suppressHydrationWarning: true,
|
|
94
|
-
suppressContentEditableWarning: true,
|
|
95
|
-
}
|
|
96
|
-
mapCompatDomProps(p, 'div')
|
|
97
|
-
expect(p).toEqual({})
|
|
98
|
-
})
|
|
99
|
-
})
|
|
@@ -1,281 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
defineComponent,
|
|
3
|
-
dispatchToErrorBoundary,
|
|
4
|
-
popErrorBoundary,
|
|
5
|
-
propagateError,
|
|
6
|
-
pushErrorBoundary,
|
|
7
|
-
runWithHooks,
|
|
8
|
-
} from '../component'
|
|
9
|
-
import { h } from '../h'
|
|
10
|
-
import { onErrorCaptured, onMount, onUnmount, onUpdate } from '../lifecycle'
|
|
11
|
-
import type { ComponentFn, LifecycleHooks, VNode } from '../types'
|
|
12
|
-
|
|
13
|
-
describe('defineComponent', () => {
|
|
14
|
-
test('returns the exact same function (identity)', () => {
|
|
15
|
-
const fn: ComponentFn = () => h('div', null)
|
|
16
|
-
expect(defineComponent(fn)).toBe(fn)
|
|
17
|
-
})
|
|
18
|
-
|
|
19
|
-
test('preserves typed props', () => {
|
|
20
|
-
const Comp = defineComponent<{ count: number }>((props) => {
|
|
21
|
-
return h('span', null, String(props.count))
|
|
22
|
-
})
|
|
23
|
-
const node = Comp({ count: 10 })
|
|
24
|
-
expect((node as VNode).type).toBe('span')
|
|
25
|
-
})
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
describe('runWithHooks', () => {
|
|
29
|
-
test('captures all lifecycle hook types', () => {
|
|
30
|
-
const mountFn = () => undefined
|
|
31
|
-
const unmountFn = () => {}
|
|
32
|
-
const updateFn = () => {}
|
|
33
|
-
const errorFn = () => true
|
|
34
|
-
|
|
35
|
-
const Comp: ComponentFn = () => {
|
|
36
|
-
onMount(mountFn)
|
|
37
|
-
onUnmount(unmountFn)
|
|
38
|
-
onUpdate(updateFn)
|
|
39
|
-
onErrorCaptured(errorFn)
|
|
40
|
-
return h('div', null)
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const { vnode, hooks } = runWithHooks(Comp, {})
|
|
44
|
-
expect(vnode).not.toBeNull()
|
|
45
|
-
expect(hooks.mount).toContain(mountFn)
|
|
46
|
-
expect(hooks.unmount).toContain(unmountFn)
|
|
47
|
-
expect(hooks.update).toContain(updateFn)
|
|
48
|
-
expect(hooks.error).toContain(errorFn)
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
test('returns null vnode for component returning null', () => {
|
|
52
|
-
const { vnode } = runWithHooks(() => null, {})
|
|
53
|
-
expect(vnode).toBeNull()
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
test('returns string vnode for component returning string', () => {
|
|
57
|
-
const { vnode } = runWithHooks(() => 'hello', {})
|
|
58
|
-
expect(vnode).toBe('hello')
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
test('clears hooks context after execution', () => {
|
|
62
|
-
const Comp: ComponentFn = () => h('div', null)
|
|
63
|
-
runWithHooks(Comp, {})
|
|
64
|
-
// After runWithHooks, lifecycle hooks should be no-ops
|
|
65
|
-
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
66
|
-
onMount(() => {})
|
|
67
|
-
expect(warnSpy).toHaveBeenCalled()
|
|
68
|
-
warnSpy.mockRestore()
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
test('clears hooks context even when component throws', () => {
|
|
72
|
-
const Comp: ComponentFn = () => {
|
|
73
|
-
throw new Error('boom')
|
|
74
|
-
}
|
|
75
|
-
expect(() => runWithHooks(Comp, {})).toThrow('boom')
|
|
76
|
-
// Should still be cleared
|
|
77
|
-
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
78
|
-
onMount(() => {})
|
|
79
|
-
expect(warnSpy).toHaveBeenCalled()
|
|
80
|
-
warnSpy.mockRestore()
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
test('passes props to component function', () => {
|
|
84
|
-
let received: unknown = null
|
|
85
|
-
runWithHooks(
|
|
86
|
-
((props: { msg: string }) => {
|
|
87
|
-
received = props
|
|
88
|
-
return null
|
|
89
|
-
}) as ComponentFn,
|
|
90
|
-
{ msg: 'hello' },
|
|
91
|
-
)
|
|
92
|
-
expect(received).toEqual({ msg: 'hello' })
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
test('captures multiple hooks of same type', () => {
|
|
96
|
-
const Comp: ComponentFn = () => {
|
|
97
|
-
onMount(() => undefined)
|
|
98
|
-
onMount(() => undefined)
|
|
99
|
-
onUnmount(() => {})
|
|
100
|
-
onUnmount(() => {})
|
|
101
|
-
return null
|
|
102
|
-
}
|
|
103
|
-
const { hooks } = runWithHooks(Comp, {})
|
|
104
|
-
expect(hooks.mount).toHaveLength(2)
|
|
105
|
-
expect(hooks.unmount).toHaveLength(2)
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
test('null hooks when component registers none (lazy allocation)', () => {
|
|
109
|
-
const { hooks } = runWithHooks(() => h('div', null), {})
|
|
110
|
-
expect(hooks.mount).toBeNull()
|
|
111
|
-
expect(hooks.unmount).toBeNull()
|
|
112
|
-
expect(hooks.update).toBeNull()
|
|
113
|
-
expect(hooks.error).toBeNull()
|
|
114
|
-
})
|
|
115
|
-
})
|
|
116
|
-
|
|
117
|
-
describe('propagateError', () => {
|
|
118
|
-
test('returns true when handler returns true', () => {
|
|
119
|
-
const hooks: LifecycleHooks = {
|
|
120
|
-
mount: [],
|
|
121
|
-
unmount: [],
|
|
122
|
-
update: [],
|
|
123
|
-
error: [() => true],
|
|
124
|
-
}
|
|
125
|
-
expect(propagateError(new Error('test'), hooks)).toBe(true)
|
|
126
|
-
})
|
|
127
|
-
|
|
128
|
-
test('returns false when no handlers', () => {
|
|
129
|
-
const hooks: LifecycleHooks = {
|
|
130
|
-
mount: [],
|
|
131
|
-
unmount: [],
|
|
132
|
-
update: [],
|
|
133
|
-
error: [],
|
|
134
|
-
}
|
|
135
|
-
expect(propagateError(new Error('test'), hooks)).toBe(false)
|
|
136
|
-
})
|
|
137
|
-
|
|
138
|
-
test('returns false when handler returns undefined', () => {
|
|
139
|
-
const hooks: LifecycleHooks = {
|
|
140
|
-
mount: [],
|
|
141
|
-
unmount: [],
|
|
142
|
-
update: [],
|
|
143
|
-
error: [() => undefined],
|
|
144
|
-
}
|
|
145
|
-
expect(propagateError(new Error('test'), hooks)).toBe(false)
|
|
146
|
-
})
|
|
147
|
-
|
|
148
|
-
test('stops at first handler returning true', () => {
|
|
149
|
-
let secondCalled = false
|
|
150
|
-
const hooks: LifecycleHooks = {
|
|
151
|
-
mount: [],
|
|
152
|
-
unmount: [],
|
|
153
|
-
update: [],
|
|
154
|
-
error: [
|
|
155
|
-
() => true,
|
|
156
|
-
() => {
|
|
157
|
-
secondCalled = true
|
|
158
|
-
return true
|
|
159
|
-
},
|
|
160
|
-
],
|
|
161
|
-
}
|
|
162
|
-
expect(propagateError('err', hooks)).toBe(true)
|
|
163
|
-
expect(secondCalled).toBe(false)
|
|
164
|
-
})
|
|
165
|
-
|
|
166
|
-
test('continues to next handler when first returns undefined', () => {
|
|
167
|
-
const calls: number[] = []
|
|
168
|
-
const hooks: LifecycleHooks = {
|
|
169
|
-
mount: [],
|
|
170
|
-
unmount: [],
|
|
171
|
-
update: [],
|
|
172
|
-
error: [
|
|
173
|
-
() => {
|
|
174
|
-
calls.push(1)
|
|
175
|
-
return undefined
|
|
176
|
-
},
|
|
177
|
-
() => {
|
|
178
|
-
calls.push(2)
|
|
179
|
-
return true
|
|
180
|
-
},
|
|
181
|
-
],
|
|
182
|
-
}
|
|
183
|
-
expect(propagateError('err', hooks)).toBe(true)
|
|
184
|
-
expect(calls).toEqual([1, 2])
|
|
185
|
-
})
|
|
186
|
-
|
|
187
|
-
test('passes the error to each handler', () => {
|
|
188
|
-
const errors: unknown[] = []
|
|
189
|
-
const hooks: LifecycleHooks = {
|
|
190
|
-
mount: [],
|
|
191
|
-
unmount: [],
|
|
192
|
-
update: [],
|
|
193
|
-
error: [
|
|
194
|
-
(err) => {
|
|
195
|
-
errors.push(err)
|
|
196
|
-
return undefined
|
|
197
|
-
},
|
|
198
|
-
(err) => {
|
|
199
|
-
errors.push(err)
|
|
200
|
-
return true
|
|
201
|
-
},
|
|
202
|
-
],
|
|
203
|
-
}
|
|
204
|
-
const testErr = new Error('propagated')
|
|
205
|
-
propagateError(testErr, hooks)
|
|
206
|
-
expect(errors).toEqual([testErr, testErr])
|
|
207
|
-
})
|
|
208
|
-
})
|
|
209
|
-
|
|
210
|
-
describe('pushErrorBoundary / popErrorBoundary / dispatchToErrorBoundary', () => {
|
|
211
|
-
afterEach(() => {
|
|
212
|
-
// Clean up any leftover boundaries — pop until empty
|
|
213
|
-
// dispatchToErrorBoundary returns false when stack is empty
|
|
214
|
-
while (dispatchToErrorBoundary('cleanup-probe')) {
|
|
215
|
-
popErrorBoundary()
|
|
216
|
-
}
|
|
217
|
-
})
|
|
218
|
-
|
|
219
|
-
test('dispatches to the most recently pushed boundary', () => {
|
|
220
|
-
let caught: unknown = null
|
|
221
|
-
pushErrorBoundary((err) => {
|
|
222
|
-
caught = err
|
|
223
|
-
return true
|
|
224
|
-
})
|
|
225
|
-
expect(dispatchToErrorBoundary('test-error')).toBe(true)
|
|
226
|
-
expect(caught).toBe('test-error')
|
|
227
|
-
popErrorBoundary()
|
|
228
|
-
})
|
|
229
|
-
|
|
230
|
-
test('returns false when no boundary is registered', () => {
|
|
231
|
-
expect(dispatchToErrorBoundary('no-boundary')).toBe(false)
|
|
232
|
-
})
|
|
233
|
-
|
|
234
|
-
test('nested boundaries — innermost catches first', () => {
|
|
235
|
-
const caught: string[] = []
|
|
236
|
-
pushErrorBoundary((err) => {
|
|
237
|
-
caught.push(`outer: ${err}`)
|
|
238
|
-
return true
|
|
239
|
-
})
|
|
240
|
-
pushErrorBoundary((err) => {
|
|
241
|
-
caught.push(`inner: ${err}`)
|
|
242
|
-
return true
|
|
243
|
-
})
|
|
244
|
-
dispatchToErrorBoundary('test')
|
|
245
|
-
expect(caught).toEqual(['inner: test'])
|
|
246
|
-
popErrorBoundary()
|
|
247
|
-
|
|
248
|
-
// After popping inner, outer should catch
|
|
249
|
-
dispatchToErrorBoundary('test2')
|
|
250
|
-
expect(caught).toEqual(['inner: test', 'outer: test2'])
|
|
251
|
-
popErrorBoundary()
|
|
252
|
-
})
|
|
253
|
-
|
|
254
|
-
test('boundary handler returning false does not propagate to outer', () => {
|
|
255
|
-
// dispatchToErrorBoundary only calls the innermost handler
|
|
256
|
-
let outerCalled = false
|
|
257
|
-
pushErrorBoundary(() => {
|
|
258
|
-
outerCalled = true
|
|
259
|
-
return true
|
|
260
|
-
})
|
|
261
|
-
pushErrorBoundary(() => false)
|
|
262
|
-
const result = dispatchToErrorBoundary('test')
|
|
263
|
-
expect(result).toBe(false)
|
|
264
|
-
expect(outerCalled).toBe(false) // outer not called — only innermost is checked
|
|
265
|
-
popErrorBoundary()
|
|
266
|
-
popErrorBoundary()
|
|
267
|
-
})
|
|
268
|
-
|
|
269
|
-
test('push and pop maintain stack correctly', () => {
|
|
270
|
-
const results: boolean[] = []
|
|
271
|
-
pushErrorBoundary(() => true)
|
|
272
|
-
pushErrorBoundary(() => true)
|
|
273
|
-
pushErrorBoundary(() => true)
|
|
274
|
-
popErrorBoundary()
|
|
275
|
-
popErrorBoundary()
|
|
276
|
-
results.push(dispatchToErrorBoundary('x'))
|
|
277
|
-
popErrorBoundary()
|
|
278
|
-
results.push(dispatchToErrorBoundary('y'))
|
|
279
|
-
expect(results).toEqual([true, false])
|
|
280
|
-
})
|
|
281
|
-
})
|