@pyreon/core 0.11.2 → 0.11.4
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 +2 -2
- package/src/tests/component.test.ts +281 -0
- package/src/tests/context.test.ts +263 -0
- package/src/tests/dynamic.test.ts +55 -0
- package/src/tests/error-boundary.test.ts +181 -0
- package/src/tests/for.test.ts +94 -0
- package/src/tests/h.test.ts +200 -0
- package/src/tests/lazy.test.ts +100 -0
- package/src/tests/lifecycle.test.ts +208 -0
- package/src/tests/map-array.test.ts +313 -0
- package/src/tests/portal.test.ts +48 -0
- package/src/tests/props-extended.test.ts +157 -0
- package/src/tests/ref.test.ts +70 -0
- package/src/tests/show.test.ts +238 -0
- package/src/tests/style.test.ts +157 -0
- package/src/tests/suspense.test.ts +139 -0
- package/src/tests/telemetry.test.ts +142 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { _resetIdCounter, createUniqueId, mergeProps, splitProps } from "../props"
|
|
2
|
+
|
|
3
|
+
describe("createUniqueId — extended", () => {
|
|
4
|
+
test("returns pyreon- prefixed string", () => {
|
|
5
|
+
const id = createUniqueId()
|
|
6
|
+
expect(id).toMatch(/^pyreon-\d+$/)
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
test("returns incrementing values", () => {
|
|
10
|
+
const id1 = createUniqueId()
|
|
11
|
+
const id2 = createUniqueId()
|
|
12
|
+
const id3 = createUniqueId()
|
|
13
|
+
const num1 = Number.parseInt(id1.replace("pyreon-", ""), 10)
|
|
14
|
+
const num2 = Number.parseInt(id2.replace("pyreon-", ""), 10)
|
|
15
|
+
const num3 = Number.parseInt(id3.replace("pyreon-", ""), 10)
|
|
16
|
+
expect(num2).toBe(num1 + 1)
|
|
17
|
+
expect(num3).toBe(num2 + 1)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
test("all IDs are unique", () => {
|
|
21
|
+
const ids = new Set<string>()
|
|
22
|
+
for (let i = 0; i < 100; i++) {
|
|
23
|
+
ids.add(createUniqueId())
|
|
24
|
+
}
|
|
25
|
+
expect(ids.size).toBe(100)
|
|
26
|
+
})
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
describe("_resetIdCounter", () => {
|
|
30
|
+
test("resets the counter so IDs restart", () => {
|
|
31
|
+
// Generate some IDs to advance counter
|
|
32
|
+
createUniqueId()
|
|
33
|
+
createUniqueId()
|
|
34
|
+
|
|
35
|
+
_resetIdCounter()
|
|
36
|
+
|
|
37
|
+
const id = createUniqueId()
|
|
38
|
+
expect(id).toBe("pyreon-1")
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test("subsequent calls after reset increment from 1", () => {
|
|
42
|
+
_resetIdCounter()
|
|
43
|
+
expect(createUniqueId()).toBe("pyreon-1")
|
|
44
|
+
expect(createUniqueId()).toBe("pyreon-2")
|
|
45
|
+
expect(createUniqueId()).toBe("pyreon-3")
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
describe("splitProps — extended", () => {
|
|
50
|
+
test("non-existent keys produce empty picked object", () => {
|
|
51
|
+
const props = { a: 1, b: 2 }
|
|
52
|
+
const [own, rest] = splitProps(props, ["c" as keyof typeof props])
|
|
53
|
+
expect(Object.keys(own)).toEqual([])
|
|
54
|
+
expect(rest).toEqual({ a: 1, b: 2 })
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test("all keys in picked leaves rest empty", () => {
|
|
58
|
+
const props = { x: 10, y: 20 }
|
|
59
|
+
const [own, rest] = splitProps(props, ["x", "y"])
|
|
60
|
+
expect(own).toEqual({ x: 10, y: 20 })
|
|
61
|
+
expect(Object.keys(rest)).toEqual([])
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test("preserves getter on rest side", () => {
|
|
65
|
+
let count = 0
|
|
66
|
+
const props = {} as Record<string, unknown>
|
|
67
|
+
Object.defineProperty(props, "reactive", {
|
|
68
|
+
get: () => ++count,
|
|
69
|
+
enumerable: true,
|
|
70
|
+
configurable: true,
|
|
71
|
+
})
|
|
72
|
+
Object.defineProperty(props, "other", {
|
|
73
|
+
value: "static",
|
|
74
|
+
enumerable: true,
|
|
75
|
+
configurable: true,
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
const [_own, rest] = splitProps(props, ["other"])
|
|
79
|
+
expect((rest as Record<string, unknown>).reactive).toBe(1)
|
|
80
|
+
expect((rest as Record<string, unknown>).reactive).toBe(2) // getter called again
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
test("handles object with undefined values", () => {
|
|
84
|
+
const props = { a: undefined, b: "defined" }
|
|
85
|
+
const [own, rest] = splitProps(props, ["a"])
|
|
86
|
+
expect(own.a).toBeUndefined()
|
|
87
|
+
expect((rest as Record<string, unknown>).b).toBe("defined")
|
|
88
|
+
})
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
describe("mergeProps — extended", () => {
|
|
92
|
+
test("single source returns copy", () => {
|
|
93
|
+
const src = { a: 1, b: 2 }
|
|
94
|
+
const result = mergeProps(src)
|
|
95
|
+
expect(result).toEqual({ a: 1, b: 2 })
|
|
96
|
+
expect(result).not.toBe(src) // should be a new object
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
test("three sources merge correctly", () => {
|
|
100
|
+
const result = mergeProps({ a: 1 }, { b: 2 }, { c: 3 })
|
|
101
|
+
expect(result).toEqual({ a: 1, b: 2, c: 3 })
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
test("later defined value overrides earlier", () => {
|
|
105
|
+
const result = mergeProps({ x: "first" }, { x: "second" }, { x: "third" })
|
|
106
|
+
expect(result.x).toBe("third")
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
test("undefined in later source does not override earlier defined value", () => {
|
|
110
|
+
const result = mergeProps({ x: "keep" }, { x: undefined as string | undefined })
|
|
111
|
+
expect(result.x).toBe("keep")
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
test("getter merging: later getter overrides earlier static when defined", () => {
|
|
115
|
+
let dynamic: string | undefined = "from-getter"
|
|
116
|
+
const getterSrc = {} as Record<string, unknown>
|
|
117
|
+
Object.defineProperty(getterSrc, "val", {
|
|
118
|
+
get: () => dynamic,
|
|
119
|
+
enumerable: true,
|
|
120
|
+
configurable: true,
|
|
121
|
+
})
|
|
122
|
+
const result = mergeProps({ val: "static" }, getterSrc)
|
|
123
|
+
expect(result.val).toBe("from-getter")
|
|
124
|
+
|
|
125
|
+
// When getter returns undefined, falls back to static
|
|
126
|
+
dynamic = undefined
|
|
127
|
+
expect(result.val).toBe("static")
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
test("two getters: later getter wins when defined, falls to earlier getter", () => {
|
|
131
|
+
let g1val: string | undefined = "g1"
|
|
132
|
+
let g2val: string | undefined = "g2"
|
|
133
|
+
|
|
134
|
+
const src1 = {} as Record<string, unknown>
|
|
135
|
+
Object.defineProperty(src1, "x", {
|
|
136
|
+
get: () => g1val,
|
|
137
|
+
enumerable: true,
|
|
138
|
+
configurable: true,
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
const src2 = {} as Record<string, unknown>
|
|
142
|
+
Object.defineProperty(src2, "x", {
|
|
143
|
+
get: () => g2val,
|
|
144
|
+
enumerable: true,
|
|
145
|
+
configurable: true,
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
const result = mergeProps(src1, src2)
|
|
149
|
+
expect(result.x).toBe("g2")
|
|
150
|
+
|
|
151
|
+
g2val = undefined
|
|
152
|
+
expect(result.x).toBe("g1")
|
|
153
|
+
|
|
154
|
+
g1val = undefined
|
|
155
|
+
expect(result.x).toBeUndefined()
|
|
156
|
+
})
|
|
157
|
+
})
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { Ref, RefCallback, RefProp } from "../ref"
|
|
2
|
+
import { createRef } from "../ref"
|
|
3
|
+
|
|
4
|
+
describe("createRef", () => {
|
|
5
|
+
test("returns object with current = null", () => {
|
|
6
|
+
const ref = createRef()
|
|
7
|
+
expect(ref.current).toBeNull()
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
test("current is mutable", () => {
|
|
11
|
+
const ref = createRef<number>()
|
|
12
|
+
ref.current = 42
|
|
13
|
+
expect(ref.current).toBe(42)
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
test("typed ref — HTMLElement", () => {
|
|
17
|
+
const ref = createRef<HTMLDivElement>()
|
|
18
|
+
expect(ref.current).toBeNull()
|
|
19
|
+
// In real code, runtime-dom sets this after mount
|
|
20
|
+
ref.current = {} as HTMLDivElement
|
|
21
|
+
expect(ref.current).not.toBeNull()
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test("typed ref — string", () => {
|
|
25
|
+
const ref = createRef<string>()
|
|
26
|
+
ref.current = "hello"
|
|
27
|
+
expect(ref.current).toBe("hello")
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
test("can be reset to null", () => {
|
|
31
|
+
const ref = createRef<number>()
|
|
32
|
+
ref.current = 99
|
|
33
|
+
expect(ref.current).toBe(99)
|
|
34
|
+
ref.current = null
|
|
35
|
+
expect(ref.current).toBeNull()
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test("each createRef returns a unique object", () => {
|
|
39
|
+
const ref1 = createRef()
|
|
40
|
+
const ref2 = createRef()
|
|
41
|
+
expect(ref1).not.toBe(ref2)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
test("ref object has exactly one property", () => {
|
|
45
|
+
const ref = createRef()
|
|
46
|
+
expect(Object.keys(ref)).toEqual(["current"])
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test("ref object shape matches Ref interface", () => {
|
|
50
|
+
const ref: Ref<number> = createRef<number>()
|
|
51
|
+
expect("current" in ref).toBe(true)
|
|
52
|
+
expect(ref.current).toBeNull()
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
describe("RefCallback type (type-level verification)", () => {
|
|
57
|
+
test("callback ref can be assigned to RefProp", () => {
|
|
58
|
+
const callback: RefCallback<HTMLElement> = (_el) => {}
|
|
59
|
+
// Type-level test: RefProp accepts both object ref and callback ref
|
|
60
|
+
const prop: RefProp<HTMLElement> = callback
|
|
61
|
+
expect(typeof prop).toBe("function")
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test("object ref can be assigned to RefProp", () => {
|
|
65
|
+
const ref = createRef<HTMLElement>()
|
|
66
|
+
const prop: RefProp<HTMLElement> = ref
|
|
67
|
+
expect(typeof prop).toBe("object")
|
|
68
|
+
expect(prop).toBe(ref)
|
|
69
|
+
})
|
|
70
|
+
})
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { h } from "../h"
|
|
2
|
+
import { Match, MatchSymbol, Show, Switch } from "../show"
|
|
3
|
+
import type { VNodeChild } from "../types"
|
|
4
|
+
|
|
5
|
+
describe("Show", () => {
|
|
6
|
+
test("returns a reactive getter (function)", () => {
|
|
7
|
+
const result = Show({ when: () => true, children: "visible" })
|
|
8
|
+
expect(typeof result).toBe("function")
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
test("getter returns children when condition is truthy", () => {
|
|
12
|
+
const getter = Show({ when: () => true, children: "visible" }) as unknown as () => VNodeChild
|
|
13
|
+
expect(getter()).toBe("visible")
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
test("getter returns null when condition is falsy and no fallback", () => {
|
|
17
|
+
const getter = Show({ when: () => false, children: "hidden" }) as unknown as () => VNodeChild
|
|
18
|
+
expect(getter()).toBeNull()
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
test("getter returns fallback when condition is falsy", () => {
|
|
22
|
+
const fb = h("span", null, "fallback")
|
|
23
|
+
const getter = Show({
|
|
24
|
+
when: () => false,
|
|
25
|
+
fallback: fb,
|
|
26
|
+
children: "main",
|
|
27
|
+
}) as unknown as () => VNodeChild
|
|
28
|
+
expect(getter()).toBe(fb)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test("reacts to condition changes", () => {
|
|
32
|
+
let flag = true
|
|
33
|
+
const getter = Show({
|
|
34
|
+
when: () => flag,
|
|
35
|
+
children: "yes",
|
|
36
|
+
fallback: "no",
|
|
37
|
+
}) as unknown as () => VNodeChild
|
|
38
|
+
expect(getter()).toBe("yes")
|
|
39
|
+
flag = false
|
|
40
|
+
expect(getter()).toBe("no")
|
|
41
|
+
flag = true
|
|
42
|
+
expect(getter()).toBe("yes")
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test("returns null for undefined children when truthy", () => {
|
|
46
|
+
const getter = Show({ when: () => true }) as unknown as () => VNodeChild
|
|
47
|
+
expect(getter()).toBeNull()
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test("returns null for undefined fallback when falsy", () => {
|
|
51
|
+
const getter = Show({ when: () => false, children: "x" }) as unknown as () => VNodeChild
|
|
52
|
+
expect(getter()).toBeNull()
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test("VNode children are preserved as-is", () => {
|
|
56
|
+
const child = h("div", null, "content")
|
|
57
|
+
const getter = Show({ when: () => true, children: child }) as unknown as () => VNodeChild
|
|
58
|
+
expect(getter()).toBe(child)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test("truthiness: non-empty string", () => {
|
|
62
|
+
const getter = Show({
|
|
63
|
+
when: () => "truthy-string",
|
|
64
|
+
children: "shown",
|
|
65
|
+
}) as unknown as () => VNodeChild
|
|
66
|
+
expect(getter()).toBe("shown")
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
test("truthiness: 0 is falsy", () => {
|
|
70
|
+
const getter = Show({
|
|
71
|
+
when: () => 0,
|
|
72
|
+
children: "shown",
|
|
73
|
+
fallback: "hidden",
|
|
74
|
+
}) as unknown as () => VNodeChild
|
|
75
|
+
expect(getter()).toBe("hidden")
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test("truthiness: empty string is falsy", () => {
|
|
79
|
+
const getter = Show({
|
|
80
|
+
when: () => "",
|
|
81
|
+
children: "shown",
|
|
82
|
+
fallback: "hidden",
|
|
83
|
+
}) as unknown as () => VNodeChild
|
|
84
|
+
expect(getter()).toBe("hidden")
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
test("truthiness: null is falsy", () => {
|
|
88
|
+
const getter = Show({
|
|
89
|
+
when: () => null,
|
|
90
|
+
children: "shown",
|
|
91
|
+
fallback: "hidden",
|
|
92
|
+
}) as unknown as () => VNodeChild
|
|
93
|
+
expect(getter()).toBe("hidden")
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
test("truthiness: object is truthy", () => {
|
|
97
|
+
const getter = Show({
|
|
98
|
+
when: () => ({ a: 1 }),
|
|
99
|
+
children: "shown",
|
|
100
|
+
}) as unknown as () => VNodeChild
|
|
101
|
+
expect(getter()).toBe("shown")
|
|
102
|
+
})
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
describe("Match", () => {
|
|
106
|
+
test("returns null (marker-only component)", () => {
|
|
107
|
+
const result = Match({ when: () => true, children: "content" })
|
|
108
|
+
expect(result).toBeNull()
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
test("MatchSymbol is a unique symbol", () => {
|
|
112
|
+
expect(typeof MatchSymbol).toBe("symbol")
|
|
113
|
+
expect(MatchSymbol.toString()).toContain("pyreon.Match")
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
describe("Switch", () => {
|
|
118
|
+
test("renders first truthy Match branch", () => {
|
|
119
|
+
const result = Switch({
|
|
120
|
+
children: [
|
|
121
|
+
h(Match, { when: () => false }, "first"),
|
|
122
|
+
h(Match, { when: () => true }, "second"),
|
|
123
|
+
h(Match, { when: () => true }, "third"),
|
|
124
|
+
],
|
|
125
|
+
})
|
|
126
|
+
const getter = result as unknown as () => VNodeChild
|
|
127
|
+
expect(getter()).toBe("second")
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
test("renders fallback when no match", () => {
|
|
131
|
+
const fb = h("p", null, "404")
|
|
132
|
+
const result = Switch({
|
|
133
|
+
fallback: fb,
|
|
134
|
+
children: [h(Match, { when: () => false }, "a"), h(Match, { when: () => false }, "b")],
|
|
135
|
+
})
|
|
136
|
+
const getter = result as unknown as () => VNodeChild
|
|
137
|
+
expect(getter()).toBe(fb)
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
test("returns null when no match and no fallback", () => {
|
|
141
|
+
const result = Switch({
|
|
142
|
+
children: [h(Match, { when: () => false }, "a")],
|
|
143
|
+
})
|
|
144
|
+
const getter = result as unknown as () => VNodeChild
|
|
145
|
+
expect(getter()).toBeNull()
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
test("handles single child (not array)", () => {
|
|
149
|
+
const result = Switch({
|
|
150
|
+
children: h(Match, { when: () => true }, "only"),
|
|
151
|
+
})
|
|
152
|
+
const getter = result as unknown as () => VNodeChild
|
|
153
|
+
expect(getter()).toBe("only")
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
test("handles no children", () => {
|
|
157
|
+
const result = Switch({})
|
|
158
|
+
const getter = result as unknown as () => VNodeChild
|
|
159
|
+
expect(getter()).toBeNull()
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
test("handles null/undefined children", () => {
|
|
163
|
+
const result = Switch({ children: null as unknown as VNodeChild })
|
|
164
|
+
const getter = result as unknown as () => VNodeChild
|
|
165
|
+
expect(getter()).toBeNull()
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
test("skips non-Match VNode children", () => {
|
|
169
|
+
const result = Switch({
|
|
170
|
+
fallback: "default",
|
|
171
|
+
children: [h("div", null, "not-a-match"), h(Match, { when: () => true }, "found")],
|
|
172
|
+
})
|
|
173
|
+
const getter = result as unknown as () => VNodeChild
|
|
174
|
+
expect(getter()).toBe("found")
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
test("skips non-object children (strings, null)", () => {
|
|
178
|
+
const result = Switch({
|
|
179
|
+
fallback: "default",
|
|
180
|
+
children: [
|
|
181
|
+
null as unknown as VNodeChild,
|
|
182
|
+
"string-child" as unknown as VNodeChild,
|
|
183
|
+
h(Match, { when: () => true }, "found"),
|
|
184
|
+
],
|
|
185
|
+
})
|
|
186
|
+
const getter = result as unknown as () => VNodeChild
|
|
187
|
+
expect(getter()).toBe("found")
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
test("reacts to condition changes", () => {
|
|
191
|
+
let a = false
|
|
192
|
+
let b = false
|
|
193
|
+
const result = Switch({
|
|
194
|
+
fallback: "none",
|
|
195
|
+
children: [h(Match, { when: () => a }, "A"), h(Match, { when: () => b }, "B")],
|
|
196
|
+
})
|
|
197
|
+
const getter = result as unknown as () => VNodeChild
|
|
198
|
+
expect(getter()).toBe("none")
|
|
199
|
+
b = true
|
|
200
|
+
expect(getter()).toBe("B")
|
|
201
|
+
a = true
|
|
202
|
+
expect(getter()).toBe("A") // first match wins
|
|
203
|
+
b = false
|
|
204
|
+
expect(getter()).toBe("A")
|
|
205
|
+
a = false
|
|
206
|
+
expect(getter()).toBe("none")
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
test("Match with multiple children returns array", () => {
|
|
210
|
+
const result = Switch({
|
|
211
|
+
children: [h(Match, { when: () => true }, "child1", "child2")],
|
|
212
|
+
})
|
|
213
|
+
const getter = result as unknown as () => VNodeChild
|
|
214
|
+
const value = getter()
|
|
215
|
+
expect(Array.isArray(value)).toBe(true)
|
|
216
|
+
expect(value).toEqual(["child1", "child2"])
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
test("Match with zero vnode.children falls back to props.children", () => {
|
|
220
|
+
const matchVNode = {
|
|
221
|
+
type: Match,
|
|
222
|
+
props: { when: () => true, children: "from-props" },
|
|
223
|
+
children: [],
|
|
224
|
+
key: null,
|
|
225
|
+
} as unknown as VNodeChild
|
|
226
|
+
const result = Switch({ children: [matchVNode] })
|
|
227
|
+
const getter = result as unknown as () => VNodeChild
|
|
228
|
+
expect(getter()).toBe("from-props")
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
test("Match with single vnode.children returns it directly (not array)", () => {
|
|
232
|
+
const result = Switch({
|
|
233
|
+
children: [h(Match, { when: () => true }, "single")],
|
|
234
|
+
})
|
|
235
|
+
const getter = result as unknown as () => VNodeChild
|
|
236
|
+
expect(getter()).toBe("single")
|
|
237
|
+
})
|
|
238
|
+
})
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { CSS_UNITLESS, cx, normalizeStyleValue, toKebabCase } from "../style"
|
|
2
|
+
|
|
3
|
+
// cx() is extensively tested in cx.test.ts — these tests cover toKebabCase,
|
|
4
|
+
// normalizeStyleValue, and CSS_UNITLESS which are used by runtime-dom/runtime-server.
|
|
5
|
+
|
|
6
|
+
describe("toKebabCase", () => {
|
|
7
|
+
test("converts camelCase to kebab-case", () => {
|
|
8
|
+
expect(toKebabCase("backgroundColor")).toBe("background-color")
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
test("handles single uppercase letter", () => {
|
|
12
|
+
expect(toKebabCase("zIndex")).toBe("z-index")
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
test("handles multiple uppercase letters", () => {
|
|
16
|
+
expect(toKebabCase("borderTopLeftRadius")).toBe("border-top-left-radius")
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
test("returns lowercase string unchanged", () => {
|
|
20
|
+
expect(toKebabCase("color")).toBe("color")
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
test("handles empty string", () => {
|
|
24
|
+
expect(toKebabCase("")).toBe("")
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
test("handles consecutive uppercase (treated individually)", () => {
|
|
28
|
+
expect(toKebabCase("MSTransform")).toBe("-m-s-transform")
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test("handles leading lowercase with single word", () => {
|
|
32
|
+
expect(toKebabCase("opacity")).toBe("opacity")
|
|
33
|
+
})
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
describe("normalizeStyleValue", () => {
|
|
37
|
+
test("appends px to numbers for non-unitless properties", () => {
|
|
38
|
+
expect(normalizeStyleValue("width", 100)).toBe("100px")
|
|
39
|
+
expect(normalizeStyleValue("height", 50)).toBe("50px")
|
|
40
|
+
expect(normalizeStyleValue("padding", 0)).toBe("0px")
|
|
41
|
+
expect(normalizeStyleValue("marginTop", 20)).toBe("20px")
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
test("does not append px to unitless properties", () => {
|
|
45
|
+
expect(normalizeStyleValue("opacity", 0.5)).toBe("0.5")
|
|
46
|
+
expect(normalizeStyleValue("zIndex", 10)).toBe("10")
|
|
47
|
+
expect(normalizeStyleValue("flexGrow", 1)).toBe("1")
|
|
48
|
+
expect(normalizeStyleValue("fontWeight", 700)).toBe("700")
|
|
49
|
+
expect(normalizeStyleValue("lineHeight", 1.5)).toBe("1.5")
|
|
50
|
+
expect(normalizeStyleValue("order", 3)).toBe("3")
|
|
51
|
+
expect(normalizeStyleValue("columns", 2)).toBe("2")
|
|
52
|
+
expect(normalizeStyleValue("flex", 1)).toBe("1")
|
|
53
|
+
expect(normalizeStyleValue("scale", 2)).toBe("2")
|
|
54
|
+
expect(normalizeStyleValue("widows", 2)).toBe("2")
|
|
55
|
+
expect(normalizeStyleValue("orphans", 3)).toBe("3")
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test("passes through string values unchanged", () => {
|
|
59
|
+
expect(normalizeStyleValue("width", "100%")).toBe("100%")
|
|
60
|
+
expect(normalizeStyleValue("color", "red")).toBe("red")
|
|
61
|
+
expect(normalizeStyleValue("display", "flex")).toBe("flex")
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test("converts non-string/non-number to string", () => {
|
|
65
|
+
expect(normalizeStyleValue("display", null)).toBe("null")
|
|
66
|
+
expect(normalizeStyleValue("display", undefined)).toBe("undefined")
|
|
67
|
+
expect(normalizeStyleValue("display", true)).toBe("true")
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
test("handles zero correctly for non-unitless props", () => {
|
|
71
|
+
expect(normalizeStyleValue("margin", 0)).toBe("0px")
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test("handles negative numbers", () => {
|
|
75
|
+
expect(normalizeStyleValue("marginLeft", -10)).toBe("-10px")
|
|
76
|
+
expect(normalizeStyleValue("zIndex", -1)).toBe("-1")
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
describe("CSS_UNITLESS", () => {
|
|
81
|
+
test("is a Set", () => {
|
|
82
|
+
expect(CSS_UNITLESS).toBeInstanceOf(Set)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
test("contains common unitless properties", () => {
|
|
86
|
+
expect(CSS_UNITLESS.has("opacity")).toBe(true)
|
|
87
|
+
expect(CSS_UNITLESS.has("zIndex")).toBe(true)
|
|
88
|
+
expect(CSS_UNITLESS.has("fontWeight")).toBe(true)
|
|
89
|
+
expect(CSS_UNITLESS.has("lineHeight")).toBe(true)
|
|
90
|
+
expect(CSS_UNITLESS.has("flex")).toBe(true)
|
|
91
|
+
expect(CSS_UNITLESS.has("flexGrow")).toBe(true)
|
|
92
|
+
expect(CSS_UNITLESS.has("flexShrink")).toBe(true)
|
|
93
|
+
expect(CSS_UNITLESS.has("order")).toBe(true)
|
|
94
|
+
expect(CSS_UNITLESS.has("columnCount")).toBe(true)
|
|
95
|
+
expect(CSS_UNITLESS.has("animationIterationCount")).toBe(true)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
test("contains SVG unitless properties", () => {
|
|
99
|
+
expect(CSS_UNITLESS.has("fillOpacity")).toBe(true)
|
|
100
|
+
expect(CSS_UNITLESS.has("floodOpacity")).toBe(true)
|
|
101
|
+
expect(CSS_UNITLESS.has("stopOpacity")).toBe(true)
|
|
102
|
+
expect(CSS_UNITLESS.has("strokeOpacity")).toBe(true)
|
|
103
|
+
expect(CSS_UNITLESS.has("strokeWidth")).toBe(true)
|
|
104
|
+
expect(CSS_UNITLESS.has("strokeMiterlimit")).toBe(true)
|
|
105
|
+
expect(CSS_UNITLESS.has("strokeDasharray")).toBe(true)
|
|
106
|
+
expect(CSS_UNITLESS.has("strokeDashoffset")).toBe(true)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
test("does not contain properties that need units", () => {
|
|
110
|
+
expect(CSS_UNITLESS.has("width")).toBe(false)
|
|
111
|
+
expect(CSS_UNITLESS.has("height")).toBe(false)
|
|
112
|
+
expect(CSS_UNITLESS.has("margin")).toBe(false)
|
|
113
|
+
expect(CSS_UNITLESS.has("padding")).toBe(false)
|
|
114
|
+
expect(CSS_UNITLESS.has("fontSize")).toBe(false)
|
|
115
|
+
expect(CSS_UNITLESS.has("borderWidth")).toBe(false)
|
|
116
|
+
expect(CSS_UNITLESS.has("top")).toBe(false)
|
|
117
|
+
expect(CSS_UNITLESS.has("left")).toBe(false)
|
|
118
|
+
})
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
describe("cx — additional edge cases", () => {
|
|
122
|
+
test("object with all false values", () => {
|
|
123
|
+
expect(cx({ a: false, b: false, c: false })).toBe("")
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
test("object with null and undefined values", () => {
|
|
127
|
+
expect(cx({ a: null, b: undefined, c: true })).toBe("c")
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
test("mixed array of numbers, strings, and objects", () => {
|
|
131
|
+
expect(cx([1, "two", { three: true, four: false }])).toBe("1 two three")
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
test("single-element array", () => {
|
|
135
|
+
expect(cx(["only"])).toBe("only")
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
test("number 0 in an array", () => {
|
|
139
|
+
expect(cx([0, "one"])).toBe("0 one")
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
test("boolean true in array is filtered", () => {
|
|
143
|
+
expect(cx([true, "visible"])).toBe("visible")
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
test("nested empty arrays", () => {
|
|
147
|
+
expect(cx([[], [[]], [[[]]]])).toBe("")
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
test("object with function returning false", () => {
|
|
151
|
+
expect(cx({ hidden: () => false })).toBe("")
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
test("single key object", () => {
|
|
155
|
+
expect(cx({ active: true })).toBe("active")
|
|
156
|
+
})
|
|
157
|
+
})
|