onejs-react 0.1.0

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,52 @@
1
+ /**
2
+ * Test setup - Mocks the Unity QuickJS runtime environment
3
+ *
4
+ * In the real environment, these globals are provided by:
5
+ * - CS: QuickJSBootstrap.js proxy to C# types
6
+ * - __eventAPI: Event registration system from QuickJSBootstrap.js
7
+ * - queueMicrotask: Polyfilled by QuickJSBootstrap.js
8
+ * - console: QuickJS built-in
9
+ */
10
+
11
+ import { vi } from 'vitest';
12
+ import { createMockCS, resetAllMocks } from './mocks';
13
+
14
+ // Store original globals (Node.js provides these)
15
+ const originalSetTimeout = globalThis.setTimeout;
16
+ const originalClearTimeout = globalThis.clearTimeout;
17
+ const originalQueueMicrotask = globalThis.queueMicrotask;
18
+
19
+ // Set up globals before each test
20
+ beforeEach(() => {
21
+ resetAllMocks();
22
+
23
+ // Create fresh mock CS global
24
+ (globalThis as any).CS = createMockCS();
25
+
26
+ // Mock event API with spies
27
+ (globalThis as any).__eventAPI = {
28
+ addEventListener: vi.fn(),
29
+ removeEventListener: vi.fn(),
30
+ removeAllEventListeners: vi.fn(),
31
+ };
32
+
33
+ // Use real console but spy on it for test assertions
34
+ // (React reconciler logs things we want to see during debugging)
35
+ (globalThis as any).console = {
36
+ log: vi.fn(),
37
+ error: vi.fn(),
38
+ warn: vi.fn(),
39
+ };
40
+
41
+ // Use real queueMicrotask - React scheduler depends on it
42
+ (globalThis as any).queueMicrotask = originalQueueMicrotask || ((cb: () => void) => Promise.resolve().then(cb));
43
+
44
+ // Use real setTimeout/clearTimeout - React scheduler depends on them
45
+ (globalThis as any).setTimeout = originalSetTimeout;
46
+ (globalThis as any).clearTimeout = originalClearTimeout;
47
+ });
48
+
49
+ // Clean up after each test
50
+ afterEach(() => {
51
+ vi.clearAllMocks();
52
+ });
@@ -0,0 +1,321 @@
1
+ /**
2
+ * Tests for style-parser.ts - style value parsing utilities
3
+ *
4
+ * Tests cover:
5
+ * - Length parsing (numbers, "px", "%", "auto")
6
+ * - Color parsing (hex, rgb, rgba, named colors)
7
+ * - parseStyleValue dispatcher
8
+ */
9
+
10
+ import { describe, it, expect } from "vitest"
11
+ import { parseLength, parseColor, parseStyleValue } from "../style-parser"
12
+ import { MockLength, MockColor, MockLengthUnit, MockStyleKeyword } from "./mocks"
13
+
14
+ describe("style-parser", () => {
15
+ describe("parseLength", () => {
16
+ it("parses number as pixels", () => {
17
+ const result = parseLength(100) as MockLength
18
+ expect(result).toBeInstanceOf(MockLength)
19
+ expect(result.value).toBe(100)
20
+ expect(result.unit).toBe(0) // Pixel
21
+ })
22
+
23
+ it("parses negative number as pixels", () => {
24
+ const result = parseLength(-50) as MockLength
25
+ expect(result).toBeInstanceOf(MockLength)
26
+ expect(result.value).toBe(-50)
27
+ })
28
+
29
+ it("parses float number as pixels", () => {
30
+ const result = parseLength(10.5) as MockLength
31
+ expect(result.value).toBe(10.5)
32
+ })
33
+
34
+ it("parses string without unit as pixels", () => {
35
+ const result = parseLength("100") as MockLength
36
+ expect(result).toBeInstanceOf(MockLength)
37
+ expect(result.value).toBe(100)
38
+ expect(result.unit).toBe(0)
39
+ })
40
+
41
+ it("parses 'px' suffix as pixels", () => {
42
+ const result = parseLength("200px") as MockLength
43
+ expect(result.value).toBe(200)
44
+ expect(result.unit).toBe(0)
45
+ })
46
+
47
+ it("parses '%' suffix as percent", () => {
48
+ const result = parseLength("50%") as MockLength
49
+ expect(result.value).toBe(50)
50
+ expect(result.unit).toBe(1) // Percent
51
+ })
52
+
53
+ it("parses negative percentage", () => {
54
+ const result = parseLength("-25%") as MockLength
55
+ expect(result.value).toBe(-25)
56
+ expect(result.unit).toBe(1)
57
+ })
58
+
59
+ it("parses decimal percentage", () => {
60
+ const result = parseLength("33.33%") as MockLength
61
+ expect(result.value).toBeCloseTo(33.33)
62
+ expect(result.unit).toBe(1)
63
+ })
64
+
65
+ it("returns StyleKeyword.Auto for 'auto'", () => {
66
+ const result = parseLength("auto")
67
+ expect(result).toBe(MockStyleKeyword.Auto)
68
+ })
69
+
70
+ it("returns StyleKeyword.None for 'none'", () => {
71
+ const result = parseLength("none")
72
+ expect(result).toBe(MockStyleKeyword.None)
73
+ })
74
+
75
+ it("returns StyleKeyword.Initial for 'initial'", () => {
76
+ const result = parseLength("initial")
77
+ expect(result).toBe(MockStyleKeyword.Initial)
78
+ })
79
+
80
+ it("handles whitespace in string values", () => {
81
+ const result = parseLength(" 100px ") as MockLength
82
+ expect(result.value).toBe(100)
83
+ })
84
+
85
+ it("is case insensitive for keywords", () => {
86
+ expect(parseLength("AUTO")).toBe(MockStyleKeyword.Auto)
87
+ expect(parseLength("Auto")).toBe(MockStyleKeyword.Auto)
88
+ })
89
+
90
+ it("returns null for invalid string", () => {
91
+ expect(parseLength("invalid")).toBeNull()
92
+ expect(parseLength("px")).toBeNull()
93
+ expect(parseLength("100em")).toBeNull()
94
+ })
95
+ })
96
+
97
+ describe("parseColor", () => {
98
+ describe("hex colors", () => {
99
+ it("parses 3-digit hex (#rgb)", () => {
100
+ const result = parseColor("#fff") as MockColor
101
+ expect(result).toBeInstanceOf(MockColor)
102
+ expect(result.r).toBeCloseTo(1)
103
+ expect(result.g).toBeCloseTo(1)
104
+ expect(result.b).toBeCloseTo(1)
105
+ expect(result.a).toBe(1)
106
+ })
107
+
108
+ it("parses 3-digit hex with colors", () => {
109
+ const result = parseColor("#f00") as MockColor
110
+ expect(result.r).toBeCloseTo(1)
111
+ expect(result.g).toBe(0)
112
+ expect(result.b).toBe(0)
113
+ })
114
+
115
+ it("parses 4-digit hex (#rgba)", () => {
116
+ const result = parseColor("#fff8") as MockColor
117
+ expect(result.r).toBeCloseTo(1)
118
+ expect(result.g).toBeCloseTo(1)
119
+ expect(result.b).toBeCloseTo(1)
120
+ expect(result.a).toBeCloseTo(0.533, 2)
121
+ })
122
+
123
+ it("parses 6-digit hex (#rrggbb)", () => {
124
+ const result = parseColor("#ff0000") as MockColor
125
+ expect(result.r).toBeCloseTo(1)
126
+ expect(result.g).toBe(0)
127
+ expect(result.b).toBe(0)
128
+ expect(result.a).toBe(1)
129
+ })
130
+
131
+ it("parses 6-digit hex with mixed values", () => {
132
+ const result = parseColor("#3498db") as MockColor
133
+ expect(result.r).toBeCloseTo(0.204, 2)
134
+ expect(result.g).toBeCloseTo(0.596, 2)
135
+ expect(result.b).toBeCloseTo(0.859, 2)
136
+ })
137
+
138
+ it("parses 8-digit hex (#rrggbbaa)", () => {
139
+ const result = parseColor("#ff000080") as MockColor
140
+ expect(result.r).toBeCloseTo(1)
141
+ expect(result.g).toBe(0)
142
+ expect(result.b).toBe(0)
143
+ expect(result.a).toBeCloseTo(0.502, 2)
144
+ })
145
+
146
+ it("handles uppercase hex", () => {
147
+ const result = parseColor("#FF0000") as MockColor
148
+ expect(result.r).toBeCloseTo(1)
149
+ })
150
+
151
+ it("returns null for invalid hex length", () => {
152
+ expect(parseColor("#ff")).toBeNull()
153
+ expect(parseColor("#fffff")).toBeNull()
154
+ expect(parseColor("#fffffff")).toBeNull()
155
+ })
156
+ })
157
+
158
+ describe("rgb/rgba colors", () => {
159
+ it("parses rgb(r, g, b)", () => {
160
+ const result = parseColor("rgb(255, 0, 0)") as MockColor
161
+ expect(result.r).toBeCloseTo(1)
162
+ expect(result.g).toBe(0)
163
+ expect(result.b).toBe(0)
164
+ expect(result.a).toBe(1)
165
+ })
166
+
167
+ it("parses rgba(r, g, b, a)", () => {
168
+ const result = parseColor("rgba(255, 128, 0, 0.5)") as MockColor
169
+ expect(result.r).toBeCloseTo(1)
170
+ expect(result.g).toBeCloseTo(0.502, 2)
171
+ expect(result.b).toBe(0)
172
+ expect(result.a).toBe(0.5)
173
+ })
174
+
175
+ it("handles whitespace in rgb", () => {
176
+ const result = parseColor("rgb( 255 , 255 , 255 )") as MockColor
177
+ expect(result.r).toBeCloseTo(1)
178
+ expect(result.g).toBeCloseTo(1)
179
+ expect(result.b).toBeCloseTo(1)
180
+ })
181
+
182
+ it("clamps values above 255", () => {
183
+ const result = parseColor("rgb(300, 0, 0)") as MockColor
184
+ expect(result.r).toBe(1)
185
+ })
186
+
187
+ it("clamps alpha above 1", () => {
188
+ const result = parseColor("rgba(255, 0, 0, 2)") as MockColor
189
+ expect(result.a).toBe(1)
190
+ })
191
+
192
+ it("parses rgb with percentage values", () => {
193
+ const result = parseColor("rgb(100%, 50%, 0%)") as MockColor
194
+ expect(result.r).toBeCloseTo(1)
195
+ expect(result.g).toBeCloseTo(0.5)
196
+ expect(result.b).toBe(0)
197
+ })
198
+ })
199
+
200
+ describe("named colors", () => {
201
+ it("parses 'red'", () => {
202
+ const result = parseColor("red") as MockColor
203
+ expect(result.r).toBe(1)
204
+ expect(result.g).toBe(0)
205
+ expect(result.b).toBe(0)
206
+ })
207
+
208
+ it("parses 'blue'", () => {
209
+ const result = parseColor("blue") as MockColor
210
+ expect(result.r).toBe(0)
211
+ expect(result.g).toBe(0)
212
+ expect(result.b).toBe(1)
213
+ })
214
+
215
+ it("parses 'transparent'", () => {
216
+ const result = parseColor("transparent") as MockColor
217
+ expect(result.a).toBe(0)
218
+ })
219
+
220
+ it("parses 'white'", () => {
221
+ const result = parseColor("white") as MockColor
222
+ expect(result.r).toBe(1)
223
+ expect(result.g).toBe(1)
224
+ expect(result.b).toBe(1)
225
+ })
226
+
227
+ it("parses 'black'", () => {
228
+ const result = parseColor("black") as MockColor
229
+ expect(result.r).toBe(0)
230
+ expect(result.g).toBe(0)
231
+ expect(result.b).toBe(0)
232
+ })
233
+
234
+ it("is case insensitive", () => {
235
+ expect(parseColor("RED")).toBeTruthy()
236
+ expect(parseColor("Red")).toBeTruthy()
237
+ })
238
+
239
+ it("returns null for unknown color names", () => {
240
+ expect(parseColor("notacolor")).toBeNull()
241
+ })
242
+ })
243
+ })
244
+
245
+ describe("parseStyleValue", () => {
246
+ it("parses length properties with numbers", () => {
247
+ const result = parseStyleValue("width", 100) as MockLength
248
+ expect(result).toBeInstanceOf(MockLength)
249
+ expect(result.value).toBe(100)
250
+ })
251
+
252
+ it("parses length properties with string values", () => {
253
+ const result = parseStyleValue("height", "50%") as MockLength
254
+ expect(result.value).toBe(50)
255
+ expect(result.unit).toBe(1) // Percent
256
+ })
257
+
258
+ it("parses padding shorthand", () => {
259
+ const result = parseStyleValue("padding", 16) as MockLength
260
+ expect(result).toBeInstanceOf(MockLength)
261
+ expect(result.value).toBe(16)
262
+ })
263
+
264
+ it("parses margin with percentage", () => {
265
+ const result = parseStyleValue("marginTop", "10%") as MockLength
266
+ expect(result.unit).toBe(1)
267
+ })
268
+
269
+ it("parses borderRadius", () => {
270
+ const result = parseStyleValue("borderRadius", 8) as MockLength
271
+ expect(result.value).toBe(8)
272
+ })
273
+
274
+ it("parses borderWidth", () => {
275
+ const result = parseStyleValue("borderWidth", 1) as MockLength
276
+ expect(result.value).toBe(1)
277
+ })
278
+
279
+ it("parses fontSize", () => {
280
+ const result = parseStyleValue("fontSize", 16) as MockLength
281
+ expect(result.value).toBe(16)
282
+ })
283
+
284
+ it("parses color properties", () => {
285
+ const result = parseStyleValue("backgroundColor", "#ff0000") as MockColor
286
+ expect(result).toBeInstanceOf(MockColor)
287
+ expect(result.r).toBeCloseTo(1)
288
+ })
289
+
290
+ it("parses borderColor", () => {
291
+ const result = parseStyleValue("borderColor", "rgba(0,0,0,0.1)") as MockColor
292
+ expect(result.a).toBeCloseTo(0.1)
293
+ })
294
+
295
+ it("parses color with named value", () => {
296
+ const result = parseStyleValue("color", "blue") as MockColor
297
+ expect(result.b).toBe(1)
298
+ })
299
+
300
+ it("passes through number properties unchanged", () => {
301
+ expect(parseStyleValue("opacity", 0.5)).toBe(0.5)
302
+ expect(parseStyleValue("flexGrow", 1)).toBe(1)
303
+ expect(parseStyleValue("flexShrink", 0)).toBe(0)
304
+ })
305
+
306
+ it("passes through enum properties unchanged", () => {
307
+ expect(parseStyleValue("flexDirection", "row")).toBe("row")
308
+ expect(parseStyleValue("display", "none")).toBe("none")
309
+ expect(parseStyleValue("position", "absolute")).toBe("absolute")
310
+ })
311
+
312
+ it("passes through unknown properties unchanged", () => {
313
+ expect(parseStyleValue("unknownProp", "value")).toBe("value")
314
+ })
315
+
316
+ it("handles null and undefined", () => {
317
+ expect(parseStyleValue("width", null)).toBeNull()
318
+ expect(parseStyleValue("width", undefined)).toBeUndefined()
319
+ })
320
+ })
321
+ })
@@ -0,0 +1,87 @@
1
+ import type { ReactElement } from 'react';
2
+ import type {
3
+ ViewProps,
4
+ LabelProps,
5
+ ButtonProps,
6
+ TextFieldProps,
7
+ ToggleProps,
8
+ SliderProps,
9
+ ScrollViewProps,
10
+ ImageProps,
11
+ ListViewProps
12
+ } from './types';
13
+
14
+ // Declare the intrinsic element types for JSX
15
+ // Using 'ojs-' prefix to avoid conflicts with HTML/SVG element names in @types/react
16
+ // For React 19 with jsx: "react-jsx", we need to augment 'react/jsx-runtime'
17
+ declare module 'react/jsx-runtime' {
18
+ namespace JSX {
19
+ interface IntrinsicElements {
20
+ 'ojs-view': ViewProps;
21
+ 'ojs-label': LabelProps;
22
+ 'ojs-button': ButtonProps;
23
+ 'ojs-textfield': TextFieldProps;
24
+ 'ojs-toggle': ToggleProps;
25
+ 'ojs-slider': SliderProps;
26
+ 'ojs-scrollview': ScrollViewProps;
27
+ 'ojs-image': ImageProps;
28
+ 'ojs-listview': ListViewProps;
29
+ }
30
+ }
31
+ }
32
+
33
+ // Also augment 'react' for compatibility
34
+ declare module 'react' {
35
+ namespace JSX {
36
+ interface IntrinsicElements {
37
+ 'ojs-view': ViewProps;
38
+ 'ojs-label': LabelProps;
39
+ 'ojs-button': ButtonProps;
40
+ 'ojs-textfield': TextFieldProps;
41
+ 'ojs-toggle': ToggleProps;
42
+ 'ojs-slider': SliderProps;
43
+ 'ojs-scrollview': ScrollViewProps;
44
+ 'ojs-image': ImageProps;
45
+ 'ojs-listview': ListViewProps;
46
+ }
47
+ }
48
+ }
49
+
50
+ // Component wrappers that provide nice capitalized names
51
+ // These return JSX elements with 'ojs-' prefixed type strings
52
+
53
+ export function View(props: ViewProps): ReactElement {
54
+ return <ojs-view {...props} />;
55
+ }
56
+
57
+ export function Label(props: LabelProps): ReactElement {
58
+ return <ojs-label {...props} />;
59
+ }
60
+
61
+ export function Button(props: ButtonProps): ReactElement {
62
+ return <ojs-button {...props} />;
63
+ }
64
+
65
+ export function TextField(props: TextFieldProps): ReactElement {
66
+ return <ojs-textfield {...props} />;
67
+ }
68
+
69
+ export function Toggle(props: ToggleProps): ReactElement {
70
+ return <ojs-toggle {...props} />;
71
+ }
72
+
73
+ export function Slider(props: SliderProps): ReactElement {
74
+ return <ojs-slider {...props} />;
75
+ }
76
+
77
+ export function ScrollView(props: ScrollViewProps): ReactElement {
78
+ return <ojs-scrollview {...props} />;
79
+ }
80
+
81
+ export function Image(props: ImageProps): ReactElement {
82
+ return <ojs-image {...props} />;
83
+ }
84
+
85
+ export function ListView(props: ListViewProps): ReactElement {
86
+ return <ojs-listview {...props} />;
87
+ }