@pyreon/ui-core 0.11.1 → 0.11.3

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,166 @@
1
+ import { describe, expect, it } from "vitest"
2
+ import hoistNonReactStatics from "../hoistNonReactStatics"
3
+
4
+ describe("hoistNonReactStatics", () => {
5
+ it("copies custom static properties from source to target", () => {
6
+ const Source = () => null
7
+ ;(Source as any).customStatic = "hello"
8
+ ;(Source as any).anotherStatic = 42
9
+
10
+ const Target = () => null
11
+
12
+ hoistNonReactStatics(Target, Source)
13
+
14
+ expect((Target as any).customStatic).toBe("hello")
15
+ expect((Target as any).anotherStatic).toBe(42)
16
+ })
17
+
18
+ it("does not copy component statics (displayName, defaultProps)", () => {
19
+ const Source = () => null
20
+ Source.displayName = "SourceComponent"
21
+ ;(Source as any).defaultProps = { bar: 1 }
22
+ ;(Source as any).customProp = "should copy"
23
+
24
+ const Target = () => null
25
+ Target.displayName = "TargetComponent"
26
+
27
+ hoistNonReactStatics(Target, Source)
28
+
29
+ expect(Target.displayName).toBe("TargetComponent")
30
+ expect((Target as any).defaultProps).toBeUndefined()
31
+ expect((Target as any).customProp).toBe("should copy")
32
+ })
33
+
34
+ it("does not copy known JS statics (name, length, prototype)", () => {
35
+ const Source = () => null
36
+ ;(Source as any).customProp = "value"
37
+
38
+ const Target = () => null
39
+ const originalName = Target.name
40
+
41
+ hoistNonReactStatics(Target, Source)
42
+
43
+ expect(Target.name).toBe(originalName)
44
+ expect((Target as any).customProp).toBe("value")
45
+ })
46
+
47
+ it("respects the excludeList", () => {
48
+ const Source = () => null
49
+ ;(Source as any).foo = "included"
50
+ ;(Source as any).bar = "excluded"
51
+ ;(Source as any).baz = "included"
52
+
53
+ const Target = () => null
54
+
55
+ hoistNonReactStatics(Target, Source, { bar: true })
56
+
57
+ expect((Target as any).foo).toBe("included")
58
+ expect((Target as any).bar).toBeUndefined()
59
+ expect((Target as any).baz).toBe("included")
60
+ })
61
+
62
+ it("returns the target component", () => {
63
+ const Source = () => null
64
+ const Target = () => null
65
+
66
+ const result = hoistNonReactStatics(Target, Source)
67
+ expect(result).toBe(Target)
68
+ })
69
+
70
+ it("handles string source (HTML tag) gracefully", () => {
71
+ const Target = () => null
72
+
73
+ const result = hoistNonReactStatics(Target, "div" as any)
74
+ expect(result).toBe(Target)
75
+ })
76
+
77
+ it("copies symbol-keyed properties", () => {
78
+ const sym = Symbol("custom")
79
+ const Source = () => null
80
+ ;(Source as any)[sym] = "symbol value"
81
+
82
+ const Target = () => null
83
+
84
+ hoistNonReactStatics(Target, Source)
85
+
86
+ expect((Target as any)[sym]).toBe("symbol value")
87
+ })
88
+
89
+ it("copies getters and setters via property descriptors", () => {
90
+ const Source = () => null
91
+ let value = 0
92
+ Object.defineProperty(Source, "counter", {
93
+ get: () => value,
94
+ set: (v) => {
95
+ value = v
96
+ },
97
+ enumerable: true,
98
+ configurable: true,
99
+ })
100
+
101
+ const Target = () => null
102
+
103
+ hoistNonReactStatics(Target, Source)
104
+
105
+ expect((Target as any).counter).toBe(0)
106
+ ;(Target as any).counter = 5
107
+ expect((Target as any).counter).toBe(5)
108
+ // shares the same backing variable
109
+ expect((Source as any).counter).toBe(5)
110
+ })
111
+
112
+ it("does not throw on non-configurable target properties", () => {
113
+ const Source = () => null
114
+ ;(Source as any).locked = "source value"
115
+
116
+ const Target = () => null
117
+ Object.defineProperty(Target, "locked", {
118
+ value: "target value",
119
+ writable: false,
120
+ configurable: false,
121
+ })
122
+
123
+ expect(() => hoistNonReactStatics(Target, Source)).not.toThrow()
124
+ expect((Target as any).locked).toBe("target value")
125
+ })
126
+
127
+ it("hoists statics from prototype chain", () => {
128
+ function Base() {
129
+ // constructor stub
130
+ }
131
+ Base.prototype = Object.create(null)
132
+ ;(Base as any).inheritedStatic = "from base"
133
+
134
+ function Source() {
135
+ // constructor stub
136
+ }
137
+ Source.prototype = Object.create(null)
138
+ Object.setPrototypeOf(Source, Base)
139
+ ;(Source as any).ownStatic = "from source"
140
+
141
+ const Target = () => null
142
+
143
+ hoistNonReactStatics(Target, Source as any)
144
+
145
+ expect((Target as any).ownStatic).toBe("from source")
146
+ expect((Target as any).inheritedStatic).toBe("from base")
147
+ })
148
+
149
+ it("works with components that have no custom statics", () => {
150
+ const Source = () => null
151
+ const Target = () => null
152
+
153
+ expect(() => hoistNonReactStatics(Target, Source)).not.toThrow()
154
+ })
155
+
156
+ it("stops prototype recursion at Object.prototype", () => {
157
+ const Source = () => null
158
+ ;(Source as any).custom = "value"
159
+ // Source's prototype is Function.prototype which has proto Object.prototype
160
+ // The recursion should walk up and stop at Object.prototype
161
+
162
+ const Target = () => null
163
+ expect(() => hoistNonReactStatics(Target, Source)).not.toThrow()
164
+ expect((Target as any).custom).toBe("value")
165
+ })
166
+ })
@@ -0,0 +1,53 @@
1
+ import { describe, expect, it } from "vitest"
2
+ import isEmpty from "../isEmpty"
3
+
4
+ describe("isEmpty", () => {
5
+ it("should return true for null", () => {
6
+ expect(isEmpty(null)).toBe(true)
7
+ })
8
+
9
+ it("should return true for undefined", () => {
10
+ expect(isEmpty(undefined)).toBe(true)
11
+ })
12
+
13
+ it("should return true for empty object", () => {
14
+ expect(isEmpty({})).toBe(true)
15
+ })
16
+
17
+ it("should return true for empty array", () => {
18
+ expect(isEmpty([])).toBe(true)
19
+ })
20
+
21
+ it("should return false for non-empty object", () => {
22
+ expect(isEmpty({ a: 1 })).toBe(false)
23
+ })
24
+
25
+ it("should return false for non-empty array", () => {
26
+ expect(isEmpty([1])).toBe(false)
27
+ })
28
+
29
+ it("should return false for array with falsy values", () => {
30
+ expect(isEmpty([0, null, undefined])).toBe(false)
31
+ })
32
+
33
+ it("should return false for object with falsy values", () => {
34
+ expect(isEmpty({ a: 0, b: null })).toBe(false)
35
+ })
36
+
37
+ it("should return true for Object.create(null) with no properties", () => {
38
+ expect(isEmpty(Object.create(null))).toBe(true)
39
+ })
40
+
41
+ it("should return false for Object.create(null) with properties", () => {
42
+ const obj = Object.create(null)
43
+ obj.a = 1
44
+ expect(isEmpty(obj)).toBe(false)
45
+ })
46
+
47
+ it("should return true for non-object truthy primitives", () => {
48
+ // Covers typeof param !== 'object' branch
49
+ expect(isEmpty(42 as any)).toBe(true)
50
+ expect(isEmpty("hello" as any)).toBe(true)
51
+ expect(isEmpty(true as any)).toBe(true)
52
+ })
53
+ })
@@ -0,0 +1,114 @@
1
+ import { describe, expect, it } from "vitest"
2
+ import isEqual from "../isEqual"
3
+
4
+ describe("isEqual", () => {
5
+ // Primitives
6
+ it("should return true for identical primitives", () => {
7
+ expect(isEqual(1, 1)).toBe(true)
8
+ expect(isEqual("a", "a")).toBe(true)
9
+ expect(isEqual(true, true)).toBe(true)
10
+ })
11
+
12
+ it("should return false for different primitives", () => {
13
+ expect(isEqual(1, 2)).toBe(false)
14
+ expect(isEqual("a", "b")).toBe(false)
15
+ expect(isEqual(true, false)).toBe(false)
16
+ })
17
+
18
+ it("should handle NaN", () => {
19
+ expect(isEqual(NaN, NaN)).toBe(true)
20
+ })
21
+
22
+ it("should distinguish 0 and -0", () => {
23
+ expect(isEqual(0, -0)).toBe(false)
24
+ })
25
+
26
+ // Null / undefined
27
+ it("should return true for null === null and undefined === undefined", () => {
28
+ expect(isEqual(null, null)).toBe(true)
29
+ expect(isEqual(undefined, undefined)).toBe(true)
30
+ })
31
+
32
+ it("should return false for null vs undefined", () => {
33
+ expect(isEqual(null, undefined)).toBe(false)
34
+ })
35
+
36
+ it("should return false for null vs object", () => {
37
+ expect(isEqual(null, {})).toBe(false)
38
+ expect(isEqual({}, null)).toBe(false)
39
+ })
40
+
41
+ // Objects
42
+ it("should return true for deeply equal objects", () => {
43
+ expect(isEqual({ a: 1, b: 2 }, { a: 1, b: 2 })).toBe(true)
44
+ })
45
+
46
+ it("should return true regardless of key order", () => {
47
+ expect(isEqual({ a: 1, b: 2 }, { b: 2, a: 1 })).toBe(true)
48
+ })
49
+
50
+ it("should return false for objects with different values", () => {
51
+ expect(isEqual({ a: 1 }, { a: 2 })).toBe(false)
52
+ })
53
+
54
+ it("should return false for objects with different keys", () => {
55
+ expect(isEqual({ a: 1 }, { b: 1 })).toBe(false)
56
+ })
57
+
58
+ it("should return false for objects with different key counts", () => {
59
+ expect(isEqual({ a: 1 }, { a: 1, b: 2 })).toBe(false)
60
+ })
61
+
62
+ // Nested objects
63
+ it("should deeply compare nested objects", () => {
64
+ const a = { a: { b: { c: 1 } } }
65
+ expect(isEqual(a, { a: { b: { c: 1 } } })).toBe(true)
66
+ expect(isEqual(a, { a: { b: { c: 2 } } })).toBe(false)
67
+ })
68
+
69
+ it("should handle nested key order differences", () => {
70
+ expect(isEqual({ x: { b: 2, a: 1 }, y: 3 }, { y: 3, x: { a: 1, b: 2 } })).toBe(true)
71
+ })
72
+
73
+ // Arrays
74
+ it("should return true for identical arrays", () => {
75
+ expect(isEqual([1, 2, 3], [1, 2, 3])).toBe(true)
76
+ })
77
+
78
+ it("should return false for arrays with different values", () => {
79
+ expect(isEqual([1, 2], [1, 3])).toBe(false)
80
+ })
81
+
82
+ it("should return false for arrays with different lengths", () => {
83
+ expect(isEqual([1, 2], [1, 2, 3])).toBe(false)
84
+ })
85
+
86
+ it("should not treat array and object as equal", () => {
87
+ expect(isEqual([1], { 0: 1 })).toBe(false)
88
+ expect(isEqual({ 0: 1 }, [1])).toBe(false)
89
+ })
90
+
91
+ // Mixed nested
92
+ it("should deeply compare arrays of objects", () => {
93
+ expect(isEqual([{ a: 1 }, { b: 2 }], [{ a: 1 }, { b: 2 }])).toBe(true)
94
+ expect(isEqual([{ a: 1 }], [{ a: 2 }])).toBe(false)
95
+ })
96
+
97
+ it("should deeply compare objects with array values", () => {
98
+ expect(isEqual({ a: [1, 2] }, { a: [1, 2] })).toBe(true)
99
+ expect(isEqual({ a: [1, 2] }, { a: [1, 3] })).toBe(false)
100
+ })
101
+
102
+ // Same reference
103
+ it("should return true for the same reference", () => {
104
+ const obj = { a: 1 }
105
+ expect(isEqual(obj, obj)).toBe(true)
106
+ })
107
+
108
+ // Type mismatches
109
+ it("should return false for different types", () => {
110
+ expect(isEqual(1, "1")).toBe(false)
111
+ expect(isEqual([], {})).toBe(false)
112
+ expect(isEqual(0, false)).toBe(false)
113
+ })
114
+ })
@@ -0,0 +1,72 @@
1
+ import type { VNode } from "@pyreon/core"
2
+ import { Fragment, h } from "@pyreon/core"
3
+ import { describe, expect, it } from "vitest"
4
+ import renderFn from "../render"
5
+
6
+ const TestComponent = (props: { label?: string }) => {
7
+ return h("span", { "data-testid": "test" }, props.label ?? "default")
8
+ }
9
+
10
+ describe("render", () => {
11
+ it("should return null for falsy content", () => {
12
+ expect(renderFn(null)).toBeNull()
13
+ expect(renderFn(undefined)).toBeNull()
14
+ expect(renderFn(false as any)).toBeNull()
15
+ expect(renderFn("" as any)).toBeNull()
16
+ })
17
+
18
+ it("should render a component with props", () => {
19
+ const result = renderFn(TestComponent, { label: "hello" }) as VNode
20
+ expect(result).toBeDefined()
21
+ expect(result.type).toBe(TestComponent)
22
+ expect(result.props.label).toBe("hello")
23
+ })
24
+
25
+ it("should render a component without props", () => {
26
+ const result = renderFn(TestComponent) as VNode
27
+ expect(result).toBeDefined()
28
+ expect(result.type).toBe(TestComponent)
29
+ })
30
+
31
+ it("should return string content as-is (treated as text)", () => {
32
+ expect(renderFn("div" as any)).toBe("div")
33
+ expect(renderFn("hello" as any)).toBe("hello")
34
+ })
35
+
36
+ it("should return primitive values as-is", () => {
37
+ expect(renderFn(42 as any)).toBe(42)
38
+ expect(renderFn(true as any)).toBe(true)
39
+ expect(renderFn("text" as any)).toBe("text")
40
+ })
41
+
42
+ it("should return arrays as-is", () => {
43
+ const arr = [h("span", { key: "1" }, "a"), h("span", { key: "2" }, "b")]
44
+ expect(renderFn(arr as any)).toBe(arr)
45
+ })
46
+
47
+ it("should return a VNode object without modification when no props", () => {
48
+ const vnode = h(TestComponent, { label: "original" })
49
+ const result = renderFn(vnode as any)
50
+ // VNode objects are returned as-is
51
+ expect(result).toBe(vnode)
52
+ })
53
+
54
+ it("should return a VNode object as-is even with props (VNode path does not merge)", () => {
55
+ const vnode = h(TestComponent, { label: "original" })
56
+ const result = renderFn(vnode as any, { label: "cloned" })
57
+ // In Pyreon's render, VNode objects hit the object branch and are returned as-is
58
+ expect(result).toBe(vnode)
59
+ })
60
+
61
+ it("should return fragment as-is", () => {
62
+ const frag = h(Fragment, null, h("span", null, "a"), h("span", null, "b"))
63
+ const result = renderFn(frag as any)
64
+ expect(result).toBe(frag)
65
+ })
66
+
67
+ it("should return plain object as-is (fallback branch)", () => {
68
+ // Plain objects are not valid elements, not primitives, not arrays
69
+ const obj = { foo: "bar" }
70
+ expect(renderFn(obj as any)).toBe(obj)
71
+ })
72
+ })
@@ -0,0 +1,113 @@
1
+ import { describe, expect, it, vi } from "vitest"
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // Minimal signal mock that stores state across calls
5
+ // ---------------------------------------------------------------------------
6
+ vi.mock("@pyreon/reactivity", () => ({
7
+ signal: <T>(initial: T) => {
8
+ let value = initial
9
+ const sig = (() => value) as (() => T) & {
10
+ set: (v: T) => void
11
+ update: (fn: (c: T) => T) => void
12
+ peek: () => T
13
+ subscribe: (listener: () => void) => () => void
14
+ direct: (updater: () => void) => () => void
15
+ label: string | undefined
16
+ debug: () => { name: string | undefined; value: T; subscriberCount: number }
17
+ }
18
+ sig.set = (v: T) => {
19
+ value = v
20
+ }
21
+ sig.update = (fn: (c: T) => T) => {
22
+ value = fn(value)
23
+ }
24
+ sig.peek = () => value
25
+ sig.subscribe = () => () => {
26
+ /* noop */
27
+ }
28
+ sig.direct = () => () => {
29
+ /* noop */
30
+ }
31
+ sig.label = undefined
32
+ sig.debug = () => ({ name: undefined, value, subscriberCount: 0 })
33
+ return sig
34
+ },
35
+ }))
36
+
37
+ import useStableValue from "../useStableValue"
38
+
39
+ describe("useStableValue", () => {
40
+ describe("primitives", () => {
41
+ it("returns the value on first call with a string", () => {
42
+ const result = useStableValue("hello")
43
+ expect(result).toBe("hello")
44
+ })
45
+
46
+ it("returns the value on first call with a number", () => {
47
+ const result = useStableValue(42)
48
+ expect(result).toBe(42)
49
+ })
50
+
51
+ it("returns the value on first call with a boolean", () => {
52
+ const result = useStableValue(true)
53
+ expect(result).toBe(true)
54
+ })
55
+
56
+ it("returns the value on first call with null", () => {
57
+ const result = useStableValue(null)
58
+ expect(result).toBeNull()
59
+ })
60
+ })
61
+
62
+ describe("objects", () => {
63
+ it("returns the object value", () => {
64
+ const obj = { a: 1, b: "two" }
65
+ const result = useStableValue(obj)
66
+ expect(result).toEqual({ a: 1, b: "two" })
67
+ })
68
+
69
+ it("returns same reference when called with deeply equal value", () => {
70
+ // Each call creates a new signal, so we verify the value is correct
71
+ const obj1 = { x: 1, y: 2 }
72
+ const result = useStableValue(obj1)
73
+ expect(result).toEqual(obj1)
74
+ })
75
+ })
76
+
77
+ describe("arrays", () => {
78
+ it("returns the array value", () => {
79
+ const arr = [1, 2, 3]
80
+ const result = useStableValue(arr)
81
+ expect(result).toEqual([1, 2, 3])
82
+ })
83
+
84
+ it("handles nested arrays", () => {
85
+ const arr = [
86
+ [1, 2],
87
+ [3, 4],
88
+ ]
89
+ const result = useStableValue(arr)
90
+ expect(result).toEqual([
91
+ [1, 2],
92
+ [3, 4],
93
+ ])
94
+ })
95
+ })
96
+
97
+ describe("signal interaction", () => {
98
+ it("creates a signal with the initial value and returns peek()", () => {
99
+ const value = { key: "value" }
100
+ const result = useStableValue(value)
101
+ expect(result).toEqual(value)
102
+ })
103
+
104
+ it("returns the initial value even for complex nested objects", () => {
105
+ const complex = {
106
+ nested: { deep: { value: 42 } },
107
+ arr: [1, [2, 3]],
108
+ }
109
+ const result = useStableValue(complex)
110
+ expect(result).toEqual(complex)
111
+ })
112
+ })
113
+ })