@pyreon/ui-core 0.11.1 → 0.11.2
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 +8 -7
- package/src/PyreonUI.tsx +138 -0
- package/src/__tests__/PyreonUI.test.tsx +81 -0
- package/src/__tests__/compose.test.ts +32 -0
- package/src/__tests__/config.test.ts +102 -0
- package/src/__tests__/context.test.tsx +70 -0
- package/src/__tests__/hoistNonReactStatics.test.tsx +166 -0
- package/src/__tests__/isEmpty.test.ts +53 -0
- package/src/__tests__/isEqual.test.ts +114 -0
- package/src/__tests__/render.test.tsx +72 -0
- package/src/__tests__/useStableValue.test.ts +113 -0
- package/src/__tests__/utils.test.ts +537 -0
- package/src/compose.ts +11 -0
- package/src/config.ts +57 -0
- package/src/context.tsx +40 -0
- package/src/hoistNonReactStatics.ts +59 -0
- package/src/html/htmlElementAttrs.ts +106 -0
- package/src/html/htmlTags.ts +151 -0
- package/src/html/index.ts +11 -0
- package/src/index.ts +55 -0
- package/src/isEmpty.ts +20 -0
- package/src/isEqual.ts +27 -0
- package/src/render.tsx +44 -0
- package/src/types.ts +5 -0
- package/src/useStableValue.ts +21 -0
- package/src/utils.ts +157 -0
|
@@ -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
|
+
})
|