@pyreon/rocketstyle 0.11.0 → 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.
Files changed (51) hide show
  1. package/package.json +14 -12
  2. package/src/__tests__/attrs.test.ts +190 -0
  3. package/src/__tests__/chaining.test.ts +86 -0
  4. package/src/__tests__/collection.test.ts +35 -0
  5. package/src/__tests__/compose.test.ts +36 -0
  6. package/src/__tests__/context.test.ts +200 -0
  7. package/src/__tests__/createLocalProvider.test.ts +248 -0
  8. package/src/__tests__/dimensions.test.ts +183 -0
  9. package/src/__tests__/e2e-styler.test.ts +291 -0
  10. package/src/__tests__/hooks.test.ts +207 -0
  11. package/src/__tests__/isRocketComponent.test.ts +48 -0
  12. package/src/__tests__/misc.test.ts +204 -0
  13. package/src/__tests__/providerConsumer.test.ts +248 -0
  14. package/src/__tests__/rocketstyleIntegration.test.ts +615 -0
  15. package/src/__tests__/themeUtils.test.ts +463 -0
  16. package/src/cache/LocalThemeManager.ts +14 -0
  17. package/src/cache/index.ts +3 -0
  18. package/src/constants/booleanTags.ts +32 -0
  19. package/src/constants/defaultDimensions.ts +23 -0
  20. package/src/constants/index.ts +44 -0
  21. package/src/context/context.ts +51 -0
  22. package/src/context/createLocalProvider.ts +84 -0
  23. package/src/context/localContext.ts +37 -0
  24. package/src/hoc/index.ts +3 -0
  25. package/src/hoc/rocketstyleAttrsHoc.ts +63 -0
  26. package/src/hooks/index.ts +4 -0
  27. package/src/hooks/usePseudoState.ts +79 -0
  28. package/src/hooks/useTheme.ts +36 -0
  29. package/src/index.ts +77 -0
  30. package/src/init.ts +93 -0
  31. package/src/isRocketComponent.ts +16 -0
  32. package/src/rocketstyle.ts +320 -0
  33. package/src/types/attrs.ts +13 -0
  34. package/src/types/config.ts +48 -0
  35. package/src/types/configuration.ts +69 -0
  36. package/src/types/dimensions.ts +106 -0
  37. package/src/types/hoc.ts +5 -0
  38. package/src/types/pseudo.ts +19 -0
  39. package/src/types/rocketComponent.ts +24 -0
  40. package/src/types/rocketstyle.ts +156 -0
  41. package/src/types/styles.ts +46 -0
  42. package/src/types/theme.ts +19 -0
  43. package/src/types/utils.ts +55 -0
  44. package/src/utils/attrs.ts +134 -0
  45. package/src/utils/chaining.ts +58 -0
  46. package/src/utils/collection.ts +9 -0
  47. package/src/utils/compose.ts +11 -0
  48. package/src/utils/dimensions.ts +126 -0
  49. package/src/utils/statics.ts +44 -0
  50. package/src/utils/styles.ts +18 -0
  51. package/src/utils/theme.ts +196 -0
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@pyreon/rocketstyle",
3
- "version": "0.11.0",
3
+ "version": "0.11.2",
4
4
  "repository": {
5
5
  "type": "git",
6
- "url": "https://github.com/pyreon/ui-system",
6
+ "url": "https://github.com/pyreon/pyreon",
7
7
  "directory": "packages/ui-system/rocketstyle"
8
8
  },
9
9
  "description": "Multi-dimensional style composition for Pyreon components",
@@ -11,10 +11,11 @@
11
11
  "type": "module",
12
12
  "sideEffects": false,
13
13
  "exports": {
14
- "bun": "./src/index.ts",
15
- "source": "./src/index.ts",
16
- "import": "./lib/index.js",
17
- "types": "./lib/index.d.ts"
14
+ ".": {
15
+ "bun": "./src/index.ts",
16
+ "import": "./lib/index.js",
17
+ "types": "./lib/index.d.ts"
18
+ }
18
19
  },
19
20
  "types": "./lib/index.d.ts",
20
21
  "main": "./lib/index.js",
@@ -23,7 +24,8 @@
23
24
  "!lib/**/*.map",
24
25
  "!lib/analysis",
25
26
  "README.md",
26
- "LICENSE"
27
+ "LICENSE",
28
+ "src"
27
29
  ],
28
30
  "engines": {
29
31
  "node": ">= 22"
@@ -42,13 +44,13 @@
42
44
  "typecheck": "tsc --noEmit"
43
45
  },
44
46
  "peerDependencies": {
45
- "@pyreon/core": "^0.11.0",
46
- "@pyreon/reactivity": "^0.11.0",
47
- "@pyreon/ui-core": "^0.11.0",
48
- "@pyreon/styler": "^0.11.0"
47
+ "@pyreon/core": "^0.11.2",
48
+ "@pyreon/reactivity": "^0.11.2",
49
+ "@pyreon/ui-core": "^0.11.2",
50
+ "@pyreon/styler": "^0.11.2"
49
51
  },
50
52
  "devDependencies": {
51
53
  "@vitus-labs/tools-rolldown": "^1.15.3",
52
- "@pyreon/typescript": "^0.11.0"
54
+ "@pyreon/typescript": "^0.11.2"
53
55
  }
54
56
  }
@@ -0,0 +1,190 @@
1
+ import {
2
+ calculateChainOptions,
3
+ calculateStylingAttrs,
4
+ pickStyledAttrs,
5
+ removeUndefinedProps,
6
+ } from "../utils/attrs"
7
+
8
+ describe("removeUndefinedProps", () => {
9
+ it("removes keys with undefined values", () => {
10
+ const result = removeUndefinedProps({ a: 1, b: undefined, c: "hello" })
11
+ expect(result).toEqual({ a: 1, c: "hello" })
12
+ })
13
+
14
+ it("keeps null values", () => {
15
+ const result = removeUndefinedProps({ a: null, b: 0 })
16
+ expect(result).toEqual({ a: null, b: 0 })
17
+ })
18
+
19
+ it("keeps all falsy non-undefined values", () => {
20
+ const result = removeUndefinedProps({ a: 0, b: "", c: false, d: null })
21
+ expect(result).toEqual({ a: 0, b: "", c: false, d: null })
22
+ })
23
+
24
+ it("returns empty for all undefined", () => {
25
+ expect(removeUndefinedProps({ a: undefined, b: undefined })).toEqual({})
26
+ })
27
+
28
+ it("returns empty for empty input", () => {
29
+ expect(removeUndefinedProps({})).toEqual({})
30
+ })
31
+ })
32
+
33
+ describe("pickStyledAttrs", () => {
34
+ it("picks keys that exist in keywords with truthy values", () => {
35
+ const result = pickStyledAttrs(
36
+ { state: "primary", size: "large", label: "hello" },
37
+ { state: true, size: true },
38
+ )
39
+ expect(result).toEqual({ state: "primary", size: "large" })
40
+ })
41
+
42
+ it("ignores falsy prop values", () => {
43
+ const result = pickStyledAttrs({ state: "", size: "large" }, { state: true, size: true })
44
+ expect(result).toEqual({ size: "large" })
45
+ })
46
+
47
+ it("returns empty when no keywords match", () => {
48
+ const result = pickStyledAttrs({ label: "hello" } as any, { state: true })
49
+ expect(result).toEqual({})
50
+ })
51
+
52
+ it("returns empty for empty props", () => {
53
+ const result = pickStyledAttrs({}, { state: true })
54
+ expect(result).toEqual({})
55
+ })
56
+ })
57
+
58
+ describe("calculateChainOptions", () => {
59
+ it("returns empty object when options is empty array", () => {
60
+ const calc = calculateChainOptions([])
61
+ expect(calc([])).toEqual({})
62
+ })
63
+
64
+ it("returns empty object when options is undefined", () => {
65
+ const calc = calculateChainOptions(undefined)
66
+ expect(calc([])).toEqual({})
67
+ })
68
+
69
+ it("evaluates chain of functions and merges via Object.assign", () => {
70
+ const fn1 = (props: any) => ({ a: 1, ...props })
71
+ const fn2 = (_props: any) => ({ b: 2 })
72
+ const calc = calculateChainOptions([fn1, fn2])
73
+ expect(calc([{ c: 3 }])).toEqual({ a: 1, b: 2, c: 3 })
74
+ })
75
+
76
+ it("later functions override earlier ones (shallow)", () => {
77
+ const fn1 = () => ({ a: 1 })
78
+ const fn2 = () => ({ a: 2 })
79
+ const calc = calculateChainOptions([fn1, fn2])
80
+ expect(calc([])).toEqual({ a: 2 })
81
+ })
82
+
83
+ it("passes all args to each function", () => {
84
+ const fn = vi.fn(() => ({}))
85
+ const calc = calculateChainOptions([fn])
86
+ calc(["arg1", "arg2"] as any)
87
+ expect(fn).toHaveBeenCalledWith("arg1", "arg2")
88
+ })
89
+ })
90
+
91
+ describe("calculateStylingAttrs", () => {
92
+ it("picks string values from props for dimensions", () => {
93
+ const calc = calculateStylingAttrs({ useBooleans: false, multiKeys: {} })
94
+ const result = calc({
95
+ props: { state: "primary", size: "large" },
96
+ dimensions: { state: {}, size: {} },
97
+ })
98
+ expect(result).toEqual({ state: "primary", size: "large" })
99
+ })
100
+
101
+ it("picks number values from props", () => {
102
+ const calc = calculateStylingAttrs({ useBooleans: false, multiKeys: {} })
103
+ const result = calc({
104
+ props: { state: 0 },
105
+ dimensions: { state: {} },
106
+ })
107
+ expect(result).toEqual({ state: 0 })
108
+ })
109
+
110
+ it("sets undefined for non-string/non-number values when booleans disabled", () => {
111
+ const calc = calculateStylingAttrs({ useBooleans: false, multiKeys: {} })
112
+ const result = calc({
113
+ props: { state: true },
114
+ dimensions: { state: {} },
115
+ })
116
+ expect(result).toEqual({ state: undefined })
117
+ })
118
+
119
+ it("allows arrays for multi-key dimensions", () => {
120
+ const calc = calculateStylingAttrs({
121
+ useBooleans: false,
122
+ multiKeys: { multiple: true },
123
+ })
124
+ const result = calc({
125
+ props: { multiple: ["a", "b"] },
126
+ dimensions: { multiple: {} },
127
+ })
128
+ expect(result).toEqual({ multiple: ["a", "b"] })
129
+ })
130
+
131
+ it("resolves boolean props when useBooleans is true (single key)", () => {
132
+ const calc = calculateStylingAttrs({
133
+ useBooleans: true,
134
+ multiKeys: {},
135
+ })
136
+ const result = calc({
137
+ props: { primary: true },
138
+ dimensions: { state: { primary: true, secondary: true } },
139
+ })
140
+ expect(result).toEqual({ state: "primary" })
141
+ })
142
+
143
+ it("resolves multi-key boolean props as array", () => {
144
+ const calc = calculateStylingAttrs({
145
+ useBooleans: true,
146
+ multiKeys: { multiple: true },
147
+ })
148
+ const result = calc({
149
+ props: { a: true, b: true },
150
+ dimensions: { multiple: { a: true, b: true, c: true } },
151
+ })
152
+ expect(result.multiple).toEqual(expect.arrayContaining(["a", "b"]))
153
+ })
154
+
155
+ it("prefers explicit string prop over boolean shorthand", () => {
156
+ const calc = calculateStylingAttrs({
157
+ useBooleans: true,
158
+ multiKeys: {},
159
+ })
160
+ const result = calc({
161
+ props: { state: "secondary", primary: true },
162
+ dimensions: { state: { primary: true, secondary: true } },
163
+ })
164
+ expect(result).toEqual({ state: "secondary" })
165
+ })
166
+
167
+ it("skips boolean keyword when prop value is falsy", () => {
168
+ const calc = calculateStylingAttrs({
169
+ useBooleans: true,
170
+ multiKeys: {},
171
+ })
172
+ const result = calc({
173
+ props: { primary: false },
174
+ dimensions: { state: { primary: true, secondary: true } },
175
+ })
176
+ expect(result.state).toBeUndefined()
177
+ })
178
+
179
+ it("skips boolean resolution when value is already set", () => {
180
+ const calc = calculateStylingAttrs({
181
+ useBooleans: true,
182
+ multiKeys: {},
183
+ })
184
+ const result = calc({
185
+ props: { state: "primary", secondary: true },
186
+ dimensions: { state: { primary: true, secondary: true } },
187
+ })
188
+ expect(result.state).toBe("primary")
189
+ })
190
+ })
@@ -0,0 +1,86 @@
1
+ import { chainOptions, chainOrOptions, chainReservedKeyOptions } from "../utils/chaining"
2
+
3
+ describe("chainOptions", () => {
4
+ it("appends function to defaults", () => {
5
+ const fn1 = () => ({ a: 1 })
6
+ const fn2 = () => ({ b: 2 })
7
+ const result = chainOptions(fn2, [fn1])
8
+ expect(result).toHaveLength(2)
9
+ expect(result[0]).toBe(fn1)
10
+ expect(result[1]).toBe(fn2)
11
+ })
12
+
13
+ it("wraps object in function and appends", () => {
14
+ const obj = { a: 1 }
15
+ const result = chainOptions(obj, [])
16
+ expect(result).toHaveLength(1)
17
+ expect(result[0]?.()).toEqual({ a: 1 })
18
+ })
19
+
20
+ it("returns defaults when opts is undefined", () => {
21
+ const fn1 = () => ({ a: 1 })
22
+ const result = chainOptions(undefined, [fn1])
23
+ expect(result).toEqual([fn1])
24
+ })
25
+
26
+ it("handles empty defaults", () => {
27
+ const fn = () => ({ a: 1 })
28
+ const result = chainOptions(fn, [])
29
+ expect(result).toHaveLength(1)
30
+ })
31
+
32
+ it("defaults to empty array when defaultOpts missing", () => {
33
+ const fn = () => ({ a: 1 })
34
+ // @ts-expect-error testing with undefined defaults
35
+ const result = chainOptions(fn, undefined)
36
+ expect(result).toHaveLength(1)
37
+ })
38
+ })
39
+
40
+ describe("chainOrOptions", () => {
41
+ it("merges opts with defaults using keys", () => {
42
+ const keys = ["a", "b", "c"] as const
43
+ const opts = { a: "new", c: "also" }
44
+ const defaults = { a: "old", b: "default", c: "orig" }
45
+ const result = chainOrOptions(keys, opts, defaults)
46
+ expect(result).toEqual({ a: "new", b: "default", c: "also" })
47
+ })
48
+
49
+ it("uses default when opt is falsy", () => {
50
+ const keys = ["a"] as const
51
+ const opts = { a: "" }
52
+ const defaults = { a: "default" }
53
+ const result = chainOrOptions(keys, opts, defaults)
54
+ expect(result).toEqual({ a: "default" })
55
+ })
56
+
57
+ it("handles missing keys in both", () => {
58
+ const keys = ["x"] as const
59
+ const result = chainOrOptions(keys, {}, {})
60
+ expect(result).toEqual({ x: undefined })
61
+ })
62
+ })
63
+
64
+ describe("chainReservedKeyOptions", () => {
65
+ it("chains options for each reserved key", () => {
66
+ const keys = ["theme", "styles"] as const
67
+ const fn1 = () => ({ a: 1 })
68
+ const fn2 = () => ({ b: 2 })
69
+ const opts = { theme: fn2 }
70
+ const defaults = { theme: [fn1], styles: [] }
71
+
72
+ const result = chainReservedKeyOptions(keys, opts, defaults)
73
+ expect(result.theme).toHaveLength(2)
74
+ expect(result.styles).toHaveLength(0)
75
+ })
76
+
77
+ it("wraps object opts into functions", () => {
78
+ const keys = ["theme"] as const
79
+ const opts = { theme: { color: "red" } }
80
+ const defaults = { theme: [] }
81
+
82
+ const result = chainReservedKeyOptions(keys, opts, defaults)
83
+ expect(result.theme).toHaveLength(1)
84
+ expect(result.theme?.[0]?.()).toEqual({ color: "red" })
85
+ })
86
+ })
@@ -0,0 +1,35 @@
1
+ import { removeNullableValues } from "../utils/collection"
2
+
3
+ describe("removeNullableValues", () => {
4
+ it("removes null values", () => {
5
+ expect(removeNullableValues({ a: 1, b: null })).toEqual({ a: 1 })
6
+ })
7
+
8
+ it("removes undefined values", () => {
9
+ expect(removeNullableValues({ a: 1, b: undefined })).toEqual({ a: 1 })
10
+ })
11
+
12
+ it("removes false values", () => {
13
+ expect(removeNullableValues({ a: 1, b: false })).toEqual({ a: 1 })
14
+ })
15
+
16
+ it("keeps truthy values", () => {
17
+ expect(removeNullableValues({ a: 1, b: "hello", c: true })).toEqual({
18
+ a: 1,
19
+ b: "hello",
20
+ c: true,
21
+ })
22
+ })
23
+
24
+ it("keeps zero and empty string", () => {
25
+ expect(removeNullableValues({ a: 0, b: "" })).toEqual({ a: 0, b: "" })
26
+ })
27
+
28
+ it("returns empty object for all nullable", () => {
29
+ expect(removeNullableValues({ a: null, b: undefined, c: false })).toEqual({})
30
+ })
31
+
32
+ it("handles empty object", () => {
33
+ expect(removeNullableValues({})).toEqual({})
34
+ })
35
+ })
@@ -0,0 +1,36 @@
1
+ import { calculateHocsFuncs } from "../utils/compose"
2
+
3
+ describe("calculateHocsFuncs", () => {
4
+ it("extracts functions from object values", () => {
5
+ const fn1 = (x: any) => x
6
+ const fn2 = (x: any) => x
7
+ const options = { a: fn1, b: fn2 }
8
+ const result = calculateHocsFuncs(options)
9
+ expect(result).toHaveLength(2)
10
+ })
11
+
12
+ it("filters out non-function values", () => {
13
+ const fn = (x: any) => x
14
+ const options = { a: fn, b: "string", c: 42, d: null }
15
+ const result = calculateHocsFuncs(options)
16
+ expect(result).toHaveLength(1)
17
+ expect(result[0]).toBe(fn)
18
+ })
19
+
20
+ it("reverses the order", () => {
21
+ const fn1 = () => "first"
22
+ const fn2 = () => "second"
23
+ const options = { a: fn1, b: fn2 }
24
+ const result = calculateHocsFuncs(options)
25
+ expect(result[0]).toBe(fn2)
26
+ expect(result[1]).toBe(fn1)
27
+ })
28
+
29
+ it("returns empty array for empty options", () => {
30
+ expect(calculateHocsFuncs({})).toEqual([])
31
+ })
32
+
33
+ it("handles undefined options", () => {
34
+ expect(calculateHocsFuncs(undefined as any)).toEqual([])
35
+ })
36
+ })
@@ -0,0 +1,200 @@
1
+ import type { VNodeChild } from "@pyreon/core"
2
+ import { useContext } from "@pyreon/core"
3
+ import { Provider as CoreProvider } from "@pyreon/ui-core"
4
+ import Provider from "../context/context"
5
+
6
+ // Mock @pyreon/core useContext to return controlled values
7
+ vi.mock("@pyreon/core", async (importOriginal) => {
8
+ const original = await importOriginal<typeof import("@pyreon/core")>()
9
+ return {
10
+ ...original,
11
+ useContext: vi.fn(() => ({})),
12
+ }
13
+ })
14
+
15
+ // Mock @pyreon/ui-core Provider and context
16
+ vi.mock("@pyreon/ui-core", async (importOriginal) => {
17
+ const original = await importOriginal<typeof import("@pyreon/ui-core")>()
18
+ return {
19
+ ...original,
20
+ Provider: vi.fn(((props: Record<string, unknown>) => ({
21
+ type: "div",
22
+ props: { ...props, "data-provider": "core" },
23
+ children: props.children,
24
+ key: null,
25
+ })) as any),
26
+ context: original.context,
27
+ }
28
+ })
29
+
30
+ const mockedUseContext = vi.mocked(useContext)
31
+ const mockedCoreProvider = vi.mocked(CoreProvider)
32
+
33
+ beforeEach(() => {
34
+ vi.clearAllMocks()
35
+ // Default: empty context
36
+ mockedUseContext.mockReturnValue({} as any)
37
+ mockedCoreProvider.mockImplementation(((props: Record<string, unknown>) => ({
38
+ type: "div",
39
+ props: { ...props, "data-provider": "core" },
40
+ children: props.children as VNodeChild,
41
+ key: null,
42
+ })) as any)
43
+ })
44
+
45
+ describe("Provider (context)", () => {
46
+ it("uses MODE_DEFAULT (light) when no mode is provided", () => {
47
+ mockedUseContext.mockReturnValue({} as any)
48
+
49
+ const children = { type: "span", props: {}, children: ["Hello"], key: null }
50
+ Provider({ children })
51
+
52
+ expect(mockedCoreProvider).toHaveBeenCalledTimes(1)
53
+ const callArgs = mockedCoreProvider.mock.calls[0]?.[0] as Record<string, unknown>
54
+ expect(callArgs.mode).toBe("light")
55
+ expect(callArgs.isLight).toBe(true)
56
+ expect(callArgs.isDark).toBe(false)
57
+ })
58
+
59
+ it("passes mode directly when inversed is false", () => {
60
+ mockedUseContext.mockReturnValue({ mode: "dark" } as any)
61
+
62
+ const children = { type: "span", props: {}, children: ["Hello"], key: null }
63
+ Provider({ children, mode: "dark", inversed: false })
64
+
65
+ const callArgs = mockedCoreProvider.mock.calls[0]?.[0] as Record<string, unknown>
66
+ expect(callArgs.mode).toBe("dark")
67
+ expect(callArgs.isDark).toBe(true)
68
+ expect(callArgs.isLight).toBe(false)
69
+ })
70
+
71
+ it("passes mode directly when inversed is undefined", () => {
72
+ const children = { type: "span", props: {}, children: ["Hello"], key: null }
73
+ Provider({ children, mode: "dark" })
74
+
75
+ const callArgs = mockedCoreProvider.mock.calls[0]?.[0] as Record<string, unknown>
76
+ expect(callArgs.mode).toBe("dark")
77
+ expect(callArgs.isDark).toBe(true)
78
+ expect(callArgs.isLight).toBe(false)
79
+ })
80
+
81
+ it("inverts light to dark when inversed is true", () => {
82
+ const children = { type: "span", props: {}, children: ["Hello"], key: null }
83
+ Provider({ children, mode: "light", inversed: true })
84
+
85
+ const callArgs = mockedCoreProvider.mock.calls[0]?.[0] as Record<string, unknown>
86
+ expect(callArgs.mode).toBe("dark")
87
+ expect(callArgs.isDark).toBe(true)
88
+ expect(callArgs.isLight).toBe(false)
89
+ })
90
+
91
+ it("inverts dark to light when inversed is true", () => {
92
+ const children = { type: "span", props: {}, children: ["Hello"], key: null }
93
+ Provider({ children, mode: "dark", inversed: true })
94
+
95
+ const callArgs = mockedCoreProvider.mock.calls[0]?.[0] as Record<string, unknown>
96
+ expect(callArgs.mode).toBe("light")
97
+ expect(callArgs.isLight).toBe(true)
98
+ expect(callArgs.isDark).toBe(false)
99
+ })
100
+
101
+ it("passes theme to provider when provided", () => {
102
+ const theme = { rootSize: 16, breakpoints: { sm: 576 } }
103
+ const children = { type: "span", props: {}, children: ["Hello"], key: null }
104
+ Provider({ children, theme })
105
+
106
+ const callArgs = mockedCoreProvider.mock.calls[0]?.[0] as Record<string, unknown>
107
+ expect(callArgs.theme).toEqual(theme)
108
+ })
109
+
110
+ it("does not pass theme key when theme is undefined", () => {
111
+ const children = { type: "span", props: {}, children: ["Hello"], key: null }
112
+ Provider({ children })
113
+
114
+ const callArgs = mockedCoreProvider.mock.calls[0]?.[0] as Record<string, unknown>
115
+ expect("theme" in callArgs).toBe(false)
116
+ })
117
+
118
+ it("uses custom provider when specified", () => {
119
+ const customProvider = vi.fn((props: Record<string, unknown>) => ({
120
+ type: "section",
121
+ props,
122
+ children: props.children as VNodeChild,
123
+ key: null,
124
+ }))
125
+
126
+ const children = { type: "span", props: {}, children: ["Hello"], key: null }
127
+ Provider({ children, provider: customProvider as any })
128
+
129
+ expect(customProvider).toHaveBeenCalledTimes(1)
130
+ // CoreProvider should NOT have been called
131
+ expect(mockedCoreProvider).not.toHaveBeenCalled()
132
+ })
133
+
134
+ it("defaults to CoreProvider when no provider prop is given", () => {
135
+ const children = { type: "span", props: {}, children: ["Hello"], key: null }
136
+ Provider({ children })
137
+
138
+ expect(mockedCoreProvider).toHaveBeenCalledTimes(1)
139
+ })
140
+
141
+ it("passes children through to the provider", () => {
142
+ const children = { type: "span", props: {}, children: ["Hello World"], key: null }
143
+ Provider({ children })
144
+
145
+ const callArgs = mockedCoreProvider.mock.calls[0]?.[0] as Record<string, unknown>
146
+ expect(callArgs.children).toBe(children)
147
+ })
148
+
149
+ it("passes provider reference to the provider call", () => {
150
+ const children = { type: "span", props: {}, children: ["Hello"], key: null }
151
+ Provider({ children })
152
+
153
+ const callArgs = mockedCoreProvider.mock.calls[0]?.[0] as Record<string, unknown>
154
+ expect(callArgs.provider).toBe(CoreProvider)
155
+ })
156
+
157
+ it("returns null when provider returns null", () => {
158
+ mockedCoreProvider.mockReturnValue(null as any)
159
+
160
+ const children = { type: "span", props: {}, children: ["Hello"], key: null }
161
+ const result = Provider({ children })
162
+
163
+ expect(result).toBeNull()
164
+ })
165
+
166
+ it("returns null when provider returns undefined", () => {
167
+ mockedCoreProvider.mockReturnValue(undefined as any)
168
+
169
+ const children = { type: "span", props: {}, children: ["Hello"], key: null }
170
+ const result = Provider({ children })
171
+
172
+ expect(result).toBeNull()
173
+ })
174
+
175
+ it("merges context values with incoming props (props take precedence)", () => {
176
+ mockedUseContext.mockReturnValue({
177
+ mode: "light",
178
+ theme: { rootSize: 12 },
179
+ } as any)
180
+
181
+ const overrideTheme = { rootSize: 20 }
182
+ const children = { type: "span", props: {}, children: ["Hello"], key: null }
183
+ Provider({ children, theme: overrideTheme, mode: "dark" })
184
+
185
+ const callArgs = mockedCoreProvider.mock.calls[0]?.[0] as Record<string, unknown>
186
+ expect(callArgs.mode).toBe("dark")
187
+ expect(callArgs.theme).toEqual(overrideTheme)
188
+ })
189
+
190
+ it("uses context mode when no mode prop is given", () => {
191
+ mockedUseContext.mockReturnValue({ mode: "dark" } as any)
192
+
193
+ const children = { type: "span", props: {}, children: ["Hello"], key: null }
194
+ Provider({ children })
195
+
196
+ const callArgs = mockedCoreProvider.mock.calls[0]?.[0] as Record<string, unknown>
197
+ expect(callArgs.mode).toBe("dark")
198
+ expect(callArgs.isDark).toBe(true)
199
+ })
200
+ })