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.
- package/package.json +43 -0
- package/src/__tests__/components.test.tsx +388 -0
- package/src/__tests__/host-config.test.ts +674 -0
- package/src/__tests__/mocks.ts +311 -0
- package/src/__tests__/renderer.test.tsx +387 -0
- package/src/__tests__/setup.ts +52 -0
- package/src/__tests__/style-parser.test.ts +321 -0
- package/src/components.tsx +87 -0
- package/src/host-config.ts +749 -0
- package/src/index.ts +54 -0
- package/src/renderer.ts +73 -0
- package/src/screen.tsx +242 -0
- package/src/style-parser.ts +288 -0
- package/src/types.ts +295 -0
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "onejs-react",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "React 19 renderer for OneJS (Unity UI Toolkit)",
|
|
5
|
+
"main": "src/index.ts",
|
|
6
|
+
"types": "src/index.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"src"
|
|
9
|
+
],
|
|
10
|
+
"keywords": [
|
|
11
|
+
"react",
|
|
12
|
+
"unity",
|
|
13
|
+
"ui-toolkit",
|
|
14
|
+
"onejs",
|
|
15
|
+
"game-ui",
|
|
16
|
+
"renderer"
|
|
17
|
+
],
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "https://github.com/nicosini/onejs-react"
|
|
21
|
+
},
|
|
22
|
+
"author": "nicosini",
|
|
23
|
+
"scripts": {
|
|
24
|
+
"typecheck": "tsc --noEmit",
|
|
25
|
+
"test": "vitest run",
|
|
26
|
+
"test:watch": "vitest",
|
|
27
|
+
"test:ui": "vitest --ui"
|
|
28
|
+
},
|
|
29
|
+
"peerDependencies": {
|
|
30
|
+
"react": "^18.0.0 || ^19.0.0"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"react-reconciler": "^0.31.0"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/react": "^19.0.0",
|
|
37
|
+
"@types/react-reconciler": "^0.28.9",
|
|
38
|
+
"react": "^19.0.0",
|
|
39
|
+
"typescript": "^5.7.0",
|
|
40
|
+
"vitest": "^2.1.0"
|
|
41
|
+
},
|
|
42
|
+
"license": "MIT"
|
|
43
|
+
}
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for component wrappers
|
|
3
|
+
*
|
|
4
|
+
* Tests verify that component wrappers:
|
|
5
|
+
* - Map to correct internal element types (ojs-*)
|
|
6
|
+
* - Pass through all props correctly
|
|
7
|
+
* - Handle all supported props for each component type
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
11
|
+
import React from 'react';
|
|
12
|
+
import { render } from '../renderer';
|
|
13
|
+
import {
|
|
14
|
+
View,
|
|
15
|
+
Label,
|
|
16
|
+
Button,
|
|
17
|
+
TextField,
|
|
18
|
+
Toggle,
|
|
19
|
+
Slider,
|
|
20
|
+
ScrollView,
|
|
21
|
+
Image,
|
|
22
|
+
} from '../components';
|
|
23
|
+
import { MockVisualElement, MockLength, MockColor, createMockContainer, flushMicrotasks, getEventAPI } from './mocks';
|
|
24
|
+
|
|
25
|
+
// Helper to extract value from style (handles both raw values and MockLength/MockColor)
|
|
26
|
+
function getStyleValue(style: unknown): unknown {
|
|
27
|
+
if (style instanceof MockLength) return style.value;
|
|
28
|
+
if (style instanceof MockColor) return style;
|
|
29
|
+
return style;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe('components', () => {
|
|
33
|
+
describe('View', () => {
|
|
34
|
+
it('renders as VisualElement', async () => {
|
|
35
|
+
const container = createMockContainer();
|
|
36
|
+
render(<View />, container as any);
|
|
37
|
+
await flushMicrotasks();
|
|
38
|
+
|
|
39
|
+
expect(container.children[0].__csType).toBe('UnityEngine.UIElements.VisualElement');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('applies style props', async () => {
|
|
43
|
+
const container = createMockContainer();
|
|
44
|
+
render(
|
|
45
|
+
<View
|
|
46
|
+
style={{
|
|
47
|
+
width: 200,
|
|
48
|
+
height: 100,
|
|
49
|
+
flexDirection: 'row',
|
|
50
|
+
padding: 10,
|
|
51
|
+
}}
|
|
52
|
+
/>,
|
|
53
|
+
container as any
|
|
54
|
+
);
|
|
55
|
+
await flushMicrotasks();
|
|
56
|
+
|
|
57
|
+
const el = container.children[0] as MockVisualElement;
|
|
58
|
+
expect(getStyleValue(el.style.width)).toBe(200);
|
|
59
|
+
expect(getStyleValue(el.style.height)).toBe(100);
|
|
60
|
+
expect(el.style.flexDirection).toBe('row');
|
|
61
|
+
expect(getStyleValue(el.style.paddingTop)).toBe(10);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('applies className', async () => {
|
|
65
|
+
const container = createMockContainer();
|
|
66
|
+
render(<View className="container main" />, container as any);
|
|
67
|
+
await flushMicrotasks();
|
|
68
|
+
|
|
69
|
+
const el = container.children[0] as MockVisualElement;
|
|
70
|
+
expect(el.hasClass('container')).toBe(true);
|
|
71
|
+
expect(el.hasClass('main')).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('registers event handlers', async () => {
|
|
75
|
+
const container = createMockContainer();
|
|
76
|
+
const onClick = vi.fn();
|
|
77
|
+
const onPointerMove = vi.fn();
|
|
78
|
+
|
|
79
|
+
render(<View onClick={onClick} onPointerMove={onPointerMove} />, container as any);
|
|
80
|
+
await flushMicrotasks();
|
|
81
|
+
|
|
82
|
+
const eventAPI = getEventAPI();
|
|
83
|
+
expect(eventAPI.addEventListener).toHaveBeenCalledWith(
|
|
84
|
+
container.children[0],
|
|
85
|
+
'click',
|
|
86
|
+
onClick
|
|
87
|
+
);
|
|
88
|
+
expect(eventAPI.addEventListener).toHaveBeenCalledWith(
|
|
89
|
+
container.children[0],
|
|
90
|
+
'pointermove',
|
|
91
|
+
onPointerMove
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('renders children', async () => {
|
|
96
|
+
const container = createMockContainer();
|
|
97
|
+
render(
|
|
98
|
+
<View>
|
|
99
|
+
<Label text="Child 1" />
|
|
100
|
+
<Label text="Child 2" />
|
|
101
|
+
</View>,
|
|
102
|
+
container as any
|
|
103
|
+
);
|
|
104
|
+
await flushMicrotasks();
|
|
105
|
+
|
|
106
|
+
const view = container.children[0] as MockVisualElement;
|
|
107
|
+
expect(view.childCount).toBe(2);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe('Label', () => {
|
|
112
|
+
it('renders as Label element', async () => {
|
|
113
|
+
const container = createMockContainer();
|
|
114
|
+
render(<Label text="Hello" />, container as any);
|
|
115
|
+
await flushMicrotasks();
|
|
116
|
+
|
|
117
|
+
expect(container.children[0].__csType).toBe('UnityEngine.UIElements.Label');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('sets text property', async () => {
|
|
121
|
+
const container = createMockContainer();
|
|
122
|
+
render(<Label text="Hello World" />, container as any);
|
|
123
|
+
await flushMicrotasks();
|
|
124
|
+
|
|
125
|
+
const el = container.children[0] as MockVisualElement;
|
|
126
|
+
expect(el.text).toBe('Hello World');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('applies styles', async () => {
|
|
130
|
+
const container = createMockContainer();
|
|
131
|
+
render(
|
|
132
|
+
<Label
|
|
133
|
+
text="Styled"
|
|
134
|
+
style={{ fontSize: 24, color: 'white' }}
|
|
135
|
+
/>,
|
|
136
|
+
container as any
|
|
137
|
+
);
|
|
138
|
+
await flushMicrotasks();
|
|
139
|
+
|
|
140
|
+
const el = container.children[0] as MockVisualElement;
|
|
141
|
+
expect(getStyleValue(el.style.fontSize)).toBe(24);
|
|
142
|
+
expect(el.style.color).toBeInstanceOf(MockColor);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe('Button', () => {
|
|
147
|
+
it('renders as Button element', async () => {
|
|
148
|
+
const container = createMockContainer();
|
|
149
|
+
render(<Button text="Click" />, container as any);
|
|
150
|
+
await flushMicrotasks();
|
|
151
|
+
|
|
152
|
+
expect(container.children[0].__csType).toBe('UnityEngine.UIElements.Button');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('sets text property', async () => {
|
|
156
|
+
const container = createMockContainer();
|
|
157
|
+
render(<Button text="Submit" />, container as any);
|
|
158
|
+
await flushMicrotasks();
|
|
159
|
+
|
|
160
|
+
const el = container.children[0] as MockVisualElement;
|
|
161
|
+
expect(el.text).toBe('Submit');
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('registers onClick handler', async () => {
|
|
165
|
+
const container = createMockContainer();
|
|
166
|
+
const handleClick = vi.fn();
|
|
167
|
+
|
|
168
|
+
render(<Button text="Click Me" onClick={handleClick} />, container as any);
|
|
169
|
+
await flushMicrotasks();
|
|
170
|
+
|
|
171
|
+
const eventAPI = getEventAPI();
|
|
172
|
+
expect(eventAPI.addEventListener).toHaveBeenCalledWith(
|
|
173
|
+
container.children[0],
|
|
174
|
+
'click',
|
|
175
|
+
handleClick
|
|
176
|
+
);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
describe('TextField', () => {
|
|
181
|
+
it('renders as TextField element', async () => {
|
|
182
|
+
const container = createMockContainer();
|
|
183
|
+
render(<TextField />, container as any);
|
|
184
|
+
await flushMicrotasks();
|
|
185
|
+
|
|
186
|
+
expect(container.children[0].__csType).toBe('UnityEngine.UIElements.TextField');
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('sets value property', async () => {
|
|
190
|
+
const container = createMockContainer();
|
|
191
|
+
render(<TextField value="initial text" />, container as any);
|
|
192
|
+
await flushMicrotasks();
|
|
193
|
+
|
|
194
|
+
const el = container.children[0] as MockVisualElement;
|
|
195
|
+
expect(el.value).toBe('initial text');
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('registers onChange handler', async () => {
|
|
199
|
+
const container = createMockContainer();
|
|
200
|
+
const handleChange = vi.fn();
|
|
201
|
+
|
|
202
|
+
render(<TextField onChange={handleChange} />, container as any);
|
|
203
|
+
await flushMicrotasks();
|
|
204
|
+
|
|
205
|
+
const eventAPI = getEventAPI();
|
|
206
|
+
expect(eventAPI.addEventListener).toHaveBeenCalledWith(
|
|
207
|
+
container.children[0],
|
|
208
|
+
'change',
|
|
209
|
+
handleChange
|
|
210
|
+
);
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
describe('Toggle', () => {
|
|
215
|
+
it('renders as Toggle element', async () => {
|
|
216
|
+
const container = createMockContainer();
|
|
217
|
+
render(<Toggle />, container as any);
|
|
218
|
+
await flushMicrotasks();
|
|
219
|
+
|
|
220
|
+
expect(container.children[0].__csType).toBe('UnityEngine.UIElements.Toggle');
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('sets value and label properties', async () => {
|
|
224
|
+
const container = createMockContainer();
|
|
225
|
+
render(<Toggle value={true} label="Enable feature" />, container as any);
|
|
226
|
+
await flushMicrotasks();
|
|
227
|
+
|
|
228
|
+
const el = container.children[0] as MockVisualElement;
|
|
229
|
+
expect(el.value).toBe(true);
|
|
230
|
+
expect(el.label).toBe('Enable feature');
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('registers onChange handler', async () => {
|
|
234
|
+
const container = createMockContainer();
|
|
235
|
+
const handleChange = vi.fn();
|
|
236
|
+
|
|
237
|
+
render(<Toggle onChange={handleChange} />, container as any);
|
|
238
|
+
await flushMicrotasks();
|
|
239
|
+
|
|
240
|
+
const eventAPI = getEventAPI();
|
|
241
|
+
expect(eventAPI.addEventListener).toHaveBeenCalledWith(
|
|
242
|
+
container.children[0],
|
|
243
|
+
'change',
|
|
244
|
+
handleChange
|
|
245
|
+
);
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
describe('Slider', () => {
|
|
250
|
+
it('renders as Slider element', async () => {
|
|
251
|
+
const container = createMockContainer();
|
|
252
|
+
render(<Slider />, container as any);
|
|
253
|
+
await flushMicrotasks();
|
|
254
|
+
|
|
255
|
+
expect(container.children[0].__csType).toBe('UnityEngine.UIElements.Slider');
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('sets value property', async () => {
|
|
259
|
+
const container = createMockContainer();
|
|
260
|
+
render(<Slider value={0.5} />, container as any);
|
|
261
|
+
await flushMicrotasks();
|
|
262
|
+
|
|
263
|
+
const el = container.children[0] as MockVisualElement;
|
|
264
|
+
expect(el.value).toBe(0.5);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('registers onChange handler', async () => {
|
|
268
|
+
const container = createMockContainer();
|
|
269
|
+
const handleChange = vi.fn();
|
|
270
|
+
|
|
271
|
+
render(<Slider onChange={handleChange} />, container as any);
|
|
272
|
+
await flushMicrotasks();
|
|
273
|
+
|
|
274
|
+
const eventAPI = getEventAPI();
|
|
275
|
+
expect(eventAPI.addEventListener).toHaveBeenCalledWith(
|
|
276
|
+
container.children[0],
|
|
277
|
+
'change',
|
|
278
|
+
handleChange
|
|
279
|
+
);
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
describe('ScrollView', () => {
|
|
284
|
+
it('renders as ScrollView element', async () => {
|
|
285
|
+
const container = createMockContainer();
|
|
286
|
+
render(<ScrollView />, container as any);
|
|
287
|
+
await flushMicrotasks();
|
|
288
|
+
|
|
289
|
+
expect(container.children[0].__csType).toBe('UnityEngine.UIElements.ScrollView');
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('renders children', async () => {
|
|
293
|
+
const container = createMockContainer();
|
|
294
|
+
render(
|
|
295
|
+
<ScrollView>
|
|
296
|
+
<View style={{ height: 1000 }}>
|
|
297
|
+
<Label text="Scrollable content" />
|
|
298
|
+
</View>
|
|
299
|
+
</ScrollView>,
|
|
300
|
+
container as any
|
|
301
|
+
);
|
|
302
|
+
await flushMicrotasks();
|
|
303
|
+
|
|
304
|
+
const scrollView = container.children[0] as MockVisualElement;
|
|
305
|
+
expect(scrollView.childCount).toBe(1);
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
describe('Image', () => {
|
|
310
|
+
it('renders as Image element', async () => {
|
|
311
|
+
const container = createMockContainer();
|
|
312
|
+
render(<Image />, container as any);
|
|
313
|
+
await flushMicrotasks();
|
|
314
|
+
|
|
315
|
+
expect(container.children[0].__csType).toBe('UnityEngine.UIElements.Image');
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it('applies styles', async () => {
|
|
319
|
+
const container = createMockContainer();
|
|
320
|
+
render(
|
|
321
|
+
<Image style={{ width: 100, height: 100 }} />,
|
|
322
|
+
container as any
|
|
323
|
+
);
|
|
324
|
+
await flushMicrotasks();
|
|
325
|
+
|
|
326
|
+
const el = container.children[0] as MockVisualElement;
|
|
327
|
+
expect(getStyleValue(el.style.width)).toBe(100);
|
|
328
|
+
expect(getStyleValue(el.style.height)).toBe(100);
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
describe('event handler mapping', () => {
|
|
333
|
+
it('maps all pointer events correctly', async () => {
|
|
334
|
+
const container = createMockContainer();
|
|
335
|
+
const handlers = {
|
|
336
|
+
onClick: vi.fn(),
|
|
337
|
+
onPointerDown: vi.fn(),
|
|
338
|
+
onPointerUp: vi.fn(),
|
|
339
|
+
onPointerMove: vi.fn(),
|
|
340
|
+
onPointerEnter: vi.fn(),
|
|
341
|
+
onPointerLeave: vi.fn(),
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
render(<View {...handlers} />, container as any);
|
|
345
|
+
await flushMicrotasks();
|
|
346
|
+
|
|
347
|
+
const eventAPI = getEventAPI();
|
|
348
|
+
const el = container.children[0];
|
|
349
|
+
|
|
350
|
+
expect(eventAPI.addEventListener).toHaveBeenCalledWith(el, 'click', handlers.onClick);
|
|
351
|
+
expect(eventAPI.addEventListener).toHaveBeenCalledWith(el, 'pointerdown', handlers.onPointerDown);
|
|
352
|
+
expect(eventAPI.addEventListener).toHaveBeenCalledWith(el, 'pointerup', handlers.onPointerUp);
|
|
353
|
+
expect(eventAPI.addEventListener).toHaveBeenCalledWith(el, 'pointermove', handlers.onPointerMove);
|
|
354
|
+
expect(eventAPI.addEventListener).toHaveBeenCalledWith(el, 'pointerenter', handlers.onPointerEnter);
|
|
355
|
+
expect(eventAPI.addEventListener).toHaveBeenCalledWith(el, 'pointerleave', handlers.onPointerLeave);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it('maps focus events correctly', async () => {
|
|
359
|
+
const container = createMockContainer();
|
|
360
|
+
const onFocus = vi.fn();
|
|
361
|
+
const onBlur = vi.fn();
|
|
362
|
+
|
|
363
|
+
render(<TextField onFocus={onFocus} onBlur={onBlur} />, container as any);
|
|
364
|
+
await flushMicrotasks();
|
|
365
|
+
|
|
366
|
+
const eventAPI = getEventAPI();
|
|
367
|
+
const el = container.children[0];
|
|
368
|
+
|
|
369
|
+
expect(eventAPI.addEventListener).toHaveBeenCalledWith(el, 'focus', onFocus);
|
|
370
|
+
expect(eventAPI.addEventListener).toHaveBeenCalledWith(el, 'blur', onBlur);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it('maps keyboard events correctly', async () => {
|
|
374
|
+
const container = createMockContainer();
|
|
375
|
+
const onKeyDown = vi.fn();
|
|
376
|
+
const onKeyUp = vi.fn();
|
|
377
|
+
|
|
378
|
+
render(<TextField onKeyDown={onKeyDown} onKeyUp={onKeyUp} />, container as any);
|
|
379
|
+
await flushMicrotasks();
|
|
380
|
+
|
|
381
|
+
const eventAPI = getEventAPI();
|
|
382
|
+
const el = container.children[0];
|
|
383
|
+
|
|
384
|
+
expect(eventAPI.addEventListener).toHaveBeenCalledWith(el, 'keydown', onKeyDown);
|
|
385
|
+
expect(eventAPI.addEventListener).toHaveBeenCalledWith(el, 'keyup', onKeyUp);
|
|
386
|
+
});
|
|
387
|
+
});
|
|
388
|
+
});
|