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,311 @@
1
+ /**
2
+ * Mock implementations for Unity UI Toolkit elements
3
+ *
4
+ * These mocks simulate the behavior of C# VisualElement and related classes
5
+ * as exposed through the QuickJS CS proxy.
6
+ */
7
+
8
+ // Track all created elements for test assertions
9
+ let createdElements: MockVisualElement[] = [];
10
+
11
+ // MARK: Unity Types
12
+
13
+ /**
14
+ * Mock Unity Color struct
15
+ */
16
+ export class MockColor {
17
+ constructor(
18
+ public r: number,
19
+ public g: number,
20
+ public b: number,
21
+ public a: number
22
+ ) {}
23
+ }
24
+
25
+ /**
26
+ * Mock Unity UIElements Length struct
27
+ */
28
+ export class MockLength {
29
+ constructor(
30
+ public value: number,
31
+ public unit: number = 0
32
+ ) {}
33
+ }
34
+
35
+ /**
36
+ * Mock LengthUnit enum
37
+ */
38
+ export const MockLengthUnit = {
39
+ Pixel: 0,
40
+ Percent: 1,
41
+ };
42
+
43
+ /**
44
+ * Mock StyleKeyword enum
45
+ */
46
+ export const MockStyleKeyword = {
47
+ Undefined: 0,
48
+ Auto: 1,
49
+ None: 2,
50
+ Initial: 3,
51
+ };
52
+
53
+ /**
54
+ * Mock VisualElement - base class for all UI Toolkit elements
55
+ */
56
+ export class MockVisualElement {
57
+ // Unique identifier (simulates Unity's instance ID via __csHandle)
58
+ __csHandle: number;
59
+ __csType: string;
60
+
61
+ // Child management
62
+ private _children: MockVisualElement[] = [];
63
+
64
+ // Style object (simulates IStyle)
65
+ style: Record<string, unknown> = {};
66
+
67
+ // Class list
68
+ private _classList: Set<string> = new Set();
69
+
70
+ // Common properties
71
+ text = '';
72
+ value: unknown = undefined;
73
+ label = '';
74
+
75
+ constructor(csType = 'UnityEngine.UIElements.VisualElement') {
76
+ this.__csHandle = Math.floor(Math.random() * 1000000);
77
+ this.__csType = csType;
78
+ createdElements.push(this);
79
+ }
80
+
81
+ // Child management methods
82
+ Add(child: MockVisualElement): void {
83
+ if (child && !this._children.includes(child)) {
84
+ this._children.push(child);
85
+ }
86
+ }
87
+
88
+ Insert(index: number, child: MockVisualElement): void {
89
+ if (child) {
90
+ // Remove if already exists
91
+ const existingIndex = this._children.indexOf(child);
92
+ if (existingIndex >= 0) {
93
+ this._children.splice(existingIndex, 1);
94
+ }
95
+ this._children.splice(index, 0, child);
96
+ }
97
+ }
98
+
99
+ Remove(child: MockVisualElement): void {
100
+ const index = this._children.indexOf(child);
101
+ if (index >= 0) {
102
+ this._children.splice(index, 1);
103
+ }
104
+ }
105
+
106
+ RemoveAt(index: number): void {
107
+ if (index >= 0 && index < this._children.length) {
108
+ this._children.splice(index, 1);
109
+ }
110
+ }
111
+
112
+ IndexOf(child: MockVisualElement): number {
113
+ return this._children.indexOf(child);
114
+ }
115
+
116
+ Clear(): void {
117
+ this._children = [];
118
+ }
119
+
120
+ // Class list methods
121
+ AddToClassList(className: string): void {
122
+ this._classList.add(className);
123
+ }
124
+
125
+ RemoveFromClassList(className: string): void {
126
+ this._classList.delete(className);
127
+ }
128
+
129
+ ClearClassList(): void {
130
+ this._classList.clear();
131
+ }
132
+
133
+ // Test helpers (not in real API)
134
+ get children(): readonly MockVisualElement[] {
135
+ return this._children;
136
+ }
137
+
138
+ get childCount(): number {
139
+ return this._children.length;
140
+ }
141
+
142
+ get classList(): ReadonlySet<string> {
143
+ return this._classList;
144
+ }
145
+
146
+ hasClass(className: string): boolean {
147
+ return this._classList.has(className);
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Mock Label element
153
+ */
154
+ export class MockLabel extends MockVisualElement {
155
+ constructor() {
156
+ super('UnityEngine.UIElements.Label');
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Mock Button element
162
+ */
163
+ export class MockButton extends MockVisualElement {
164
+ constructor() {
165
+ super('UnityEngine.UIElements.Button');
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Mock TextField element
171
+ */
172
+ export class MockTextField extends MockVisualElement {
173
+ constructor() {
174
+ super('UnityEngine.UIElements.TextField');
175
+ this.value = '';
176
+ }
177
+ }
178
+
179
+ /**
180
+ * Mock Toggle element
181
+ */
182
+ export class MockToggle extends MockVisualElement {
183
+ constructor() {
184
+ super('UnityEngine.UIElements.Toggle');
185
+ this.value = false;
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Mock Slider element
191
+ */
192
+ export class MockSlider extends MockVisualElement {
193
+ constructor() {
194
+ super('UnityEngine.UIElements.Slider');
195
+ this.value = 0;
196
+ }
197
+ }
198
+
199
+ /**
200
+ * Mock ScrollView element
201
+ */
202
+ export class MockScrollView extends MockVisualElement {
203
+ constructor() {
204
+ super('UnityEngine.UIElements.ScrollView');
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Mock Image element
210
+ */
211
+ export class MockImage extends MockVisualElement {
212
+ constructor() {
213
+ super('UnityEngine.UIElements.Image');
214
+ }
215
+ }
216
+
217
+ /**
218
+ * Create the mock CS global object that mirrors QuickJSBootstrap.js proxy
219
+ */
220
+ export function createMockCS() {
221
+ return {
222
+ UnityEngine: {
223
+ // Core types
224
+ Color: MockColor,
225
+ // UI Elements
226
+ UIElements: {
227
+ VisualElement: MockVisualElement,
228
+ Label: MockLabel,
229
+ Button: MockButton,
230
+ TextField: MockTextField,
231
+ Toggle: MockToggle,
232
+ Slider: MockSlider,
233
+ ScrollView: MockScrollView,
234
+ Image: MockImage,
235
+ // Style types
236
+ Length: MockLength,
237
+ LengthUnit: MockLengthUnit,
238
+ StyleKeyword: MockStyleKeyword,
239
+ },
240
+ },
241
+ };
242
+ }
243
+
244
+ /**
245
+ * Get all elements created during the test
246
+ */
247
+ export function getCreatedElements(): readonly MockVisualElement[] {
248
+ return createdElements;
249
+ }
250
+
251
+ /**
252
+ * Find a created element by its handle
253
+ */
254
+ export function findElementByHandle(handle: number): MockVisualElement | undefined {
255
+ return createdElements.find((el) => el.__csHandle === handle);
256
+ }
257
+
258
+ /**
259
+ * Reset all mocks - call this before each test
260
+ */
261
+ export function resetAllMocks(): void {
262
+ createdElements = [];
263
+ }
264
+
265
+ /**
266
+ * Create a mock container for render() tests
267
+ */
268
+ export function createMockContainer(): MockVisualElement {
269
+ return new MockVisualElement('Container');
270
+ }
271
+
272
+ /**
273
+ * Helper to wait for React to flush updates
274
+ * React uses microtasks for scheduling, so we need to flush the microtask queue
275
+ */
276
+ export async function flushMicrotasks(): Promise<void> {
277
+ // Flush multiple rounds of microtasks to handle nested scheduling
278
+ // React's reconciler needs more iterations to flush all work
279
+ for (let i = 0; i < 50; i++) {
280
+ await Promise.resolve();
281
+ // Also allow any setTimeout callbacks to run
282
+ await new Promise(resolve => setImmediate ? setImmediate(resolve) : setTimeout(resolve, 0));
283
+ }
284
+ }
285
+
286
+ /**
287
+ * Wait for a specific condition to be true, with timeout
288
+ */
289
+ export async function waitFor(
290
+ condition: () => boolean,
291
+ { timeout = 1000, interval = 10 } = {}
292
+ ): Promise<void> {
293
+ const start = Date.now();
294
+ while (!condition()) {
295
+ if (Date.now() - start > timeout) {
296
+ throw new Error('waitFor timed out');
297
+ }
298
+ await new Promise(resolve => setTimeout(resolve, interval));
299
+ }
300
+ }
301
+
302
+ /**
303
+ * Helper to get the __eventAPI mock for assertions
304
+ */
305
+ export function getEventAPI() {
306
+ return (globalThis as any).__eventAPI as {
307
+ addEventListener: ReturnType<typeof import('vitest').vi.fn>;
308
+ removeEventListener: ReturnType<typeof import('vitest').vi.fn>;
309
+ removeAllEventListeners: ReturnType<typeof import('vitest').vi.fn>;
310
+ };
311
+ }
@@ -0,0 +1,387 @@
1
+ /**
2
+ * Integration tests for the React renderer
3
+ *
4
+ * Tests cover:
5
+ * - render() and unmount() functions
6
+ * - Full React tree rendering
7
+ * - Re-renders and updates
8
+ * - Component lifecycle
9
+ */
10
+
11
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
12
+ import React, { useState, useEffect } from 'react';
13
+ import { render, unmount, getRoot } from '../renderer';
14
+ import { View, Label, Button } from '../components';
15
+ import { MockVisualElement, MockLength, MockColor, createMockContainer, flushMicrotasks, getEventAPI } from './mocks';
16
+
17
+ // Helper to extract value from style (handles both raw values and MockLength/MockColor)
18
+ function getStyleValue(style: unknown): unknown {
19
+ if (style instanceof MockLength) return style.value;
20
+ if (style instanceof MockColor) return style;
21
+ return style;
22
+ }
23
+
24
+ describe('renderer', () => {
25
+ describe('render()', () => {
26
+ it('renders a simple element to container', async () => {
27
+ const container = createMockContainer();
28
+
29
+ render(<ojs-view />, container as any);
30
+ await flushMicrotasks();
31
+
32
+ expect(container.childCount).toBe(1);
33
+ expect(container.children[0].__csType).toBe('UnityEngine.UIElements.VisualElement');
34
+ });
35
+
36
+ it('renders nested elements', async () => {
37
+ const container = createMockContainer();
38
+
39
+ render(
40
+ <ojs-view>
41
+ <ojs-label text="Hello" />
42
+ <ojs-button text="Click" />
43
+ </ojs-view>,
44
+ container as any
45
+ );
46
+ await flushMicrotasks();
47
+
48
+ expect(container.childCount).toBe(1);
49
+ const view = container.children[0] as MockVisualElement;
50
+ expect(view.childCount).toBe(2);
51
+ expect(view.children[0].__csType).toBe('UnityEngine.UIElements.Label');
52
+ expect(view.children[1].__csType).toBe('UnityEngine.UIElements.Button');
53
+ });
54
+
55
+ it('renders with styles', async () => {
56
+ const container = createMockContainer();
57
+
58
+ render(
59
+ <ojs-view style={{ width: 100, height: 50, backgroundColor: 'blue' }} />,
60
+ container as any
61
+ );
62
+ await flushMicrotasks();
63
+
64
+ const view = container.children[0] as MockVisualElement;
65
+ expect(getStyleValue(view.style.width)).toBe(100);
66
+ expect(getStyleValue(view.style.height)).toBe(50);
67
+ expect(view.style.backgroundColor).toBeInstanceOf(MockColor);
68
+ });
69
+
70
+ it('renders with className', async () => {
71
+ const container = createMockContainer();
72
+
73
+ render(<ojs-view className="foo bar" />, container as any);
74
+ await flushMicrotasks();
75
+
76
+ const view = container.children[0] as MockVisualElement;
77
+ expect(view.hasClass('foo')).toBe(true);
78
+ expect(view.hasClass('bar')).toBe(true);
79
+ });
80
+
81
+ it('creates a root on first render', async () => {
82
+ const container = createMockContainer();
83
+
84
+ expect(getRoot(container as any)).toBeUndefined();
85
+
86
+ render(<ojs-view />, container as any);
87
+ await flushMicrotasks();
88
+
89
+ expect(getRoot(container as any)).toBeDefined();
90
+ });
91
+
92
+ it('reuses root on re-render to same container', async () => {
93
+ const container = createMockContainer();
94
+
95
+ render(<ojs-view />, container as any);
96
+ await flushMicrotasks();
97
+ const firstRoot = getRoot(container as any);
98
+
99
+ render(<ojs-label text="Updated" />, container as any);
100
+ await flushMicrotasks();
101
+ const secondRoot = getRoot(container as any);
102
+
103
+ expect(secondRoot).toBe(firstRoot);
104
+ });
105
+ });
106
+
107
+ describe('unmount()', () => {
108
+ it('removes rendered content from container', async () => {
109
+ const container = createMockContainer();
110
+
111
+ render(<ojs-view />, container as any);
112
+ await flushMicrotasks();
113
+ expect(container.childCount).toBe(1);
114
+
115
+ unmount(container as any);
116
+ await flushMicrotasks();
117
+
118
+ // Container should be cleared
119
+ expect(container.childCount).toBe(0);
120
+ });
121
+
122
+ it('removes the root reference', async () => {
123
+ const container = createMockContainer();
124
+
125
+ render(<ojs-view />, container as any);
126
+ await flushMicrotasks();
127
+ expect(getRoot(container as any)).toBeDefined();
128
+
129
+ unmount(container as any);
130
+ await flushMicrotasks();
131
+
132
+ expect(getRoot(container as any)).toBeUndefined();
133
+ });
134
+
135
+ it('does nothing when unmounting non-rendered container', () => {
136
+ const container = createMockContainer();
137
+
138
+ // Should not throw
139
+ expect(() => unmount(container as any)).not.toThrow();
140
+ });
141
+ });
142
+
143
+ describe('component wrappers', () => {
144
+ it('renders View component', async () => {
145
+ const container = createMockContainer();
146
+
147
+ render(<View style={{ padding: 10 }} />, container as any);
148
+ await flushMicrotasks();
149
+
150
+ const view = container.children[0] as MockVisualElement;
151
+ expect(view.__csType).toBe('UnityEngine.UIElements.VisualElement');
152
+ expect(getStyleValue(view.style.paddingTop)).toBe(10);
153
+ });
154
+
155
+ it('renders Label component', async () => {
156
+ const container = createMockContainer();
157
+
158
+ render(<Label text="Hello World" />, container as any);
159
+ await flushMicrotasks();
160
+
161
+ const label = container.children[0] as MockVisualElement;
162
+ expect(label.__csType).toBe('UnityEngine.UIElements.Label');
163
+ expect(label.text).toBe('Hello World');
164
+ });
165
+
166
+ it('renders Button component with onClick', async () => {
167
+ const container = createMockContainer();
168
+ const handleClick = vi.fn();
169
+
170
+ render(<Button text="Click Me" onClick={handleClick} />, container as any);
171
+ await flushMicrotasks();
172
+
173
+ const button = container.children[0] as MockVisualElement;
174
+ expect(button.__csType).toBe('UnityEngine.UIElements.Button');
175
+ expect(button.text).toBe('Click Me');
176
+
177
+ // Verify event was registered
178
+ const eventAPI = getEventAPI();
179
+ expect(eventAPI.addEventListener).toHaveBeenCalledWith(
180
+ button,
181
+ 'click',
182
+ handleClick
183
+ );
184
+ });
185
+ });
186
+
187
+ describe('React state updates', () => {
188
+ it('updates when state changes', async () => {
189
+ const container = createMockContainer();
190
+ let setCount: (n: number) => void;
191
+
192
+ function Counter() {
193
+ const [count, _setCount] = useState(0);
194
+ setCount = _setCount;
195
+ return <ojs-label text={`Count: ${count}`} />;
196
+ }
197
+
198
+ render(<Counter />, container as any);
199
+ await flushMicrotasks();
200
+
201
+ const label = container.children[0] as MockVisualElement;
202
+ expect(label.text).toBe('Count: 0');
203
+
204
+ // Update state
205
+ setCount!(5);
206
+ await flushMicrotasks();
207
+
208
+ expect(label.text).toBe('Count: 5');
209
+ });
210
+
211
+ it('updates styles when props change', async () => {
212
+ const container = createMockContainer();
213
+ let setWidth: (n: number) => void;
214
+
215
+ function ResizableBox() {
216
+ const [width, _setWidth] = useState(100);
217
+ setWidth = _setWidth;
218
+ return <ojs-view style={{ width, height: 50 }} />;
219
+ }
220
+
221
+ render(<ResizableBox />, container as any);
222
+ await flushMicrotasks();
223
+
224
+ const view = container.children[0] as MockVisualElement;
225
+ expect(getStyleValue(view.style.width)).toBe(100);
226
+
227
+ setWidth!(200);
228
+ await flushMicrotasks();
229
+
230
+ expect(getStyleValue(view.style.width)).toBe(200);
231
+ });
232
+
233
+ it('removes style properties when they are removed from props', async () => {
234
+ const container = createMockContainer();
235
+ let setHasBackground: (b: boolean) => void;
236
+
237
+ function ConditionalStyle() {
238
+ const [hasBackground, _setHasBackground] = useState(true);
239
+ setHasBackground = _setHasBackground;
240
+ return (
241
+ <ojs-view
242
+ style={hasBackground ? { backgroundColor: 'red', width: 100 } : { width: 100 }}
243
+ />
244
+ );
245
+ }
246
+
247
+ render(<ConditionalStyle />, container as any);
248
+ await flushMicrotasks();
249
+
250
+ const view = container.children[0] as MockVisualElement;
251
+ expect(view.style.backgroundColor).toBeInstanceOf(MockColor);
252
+ expect(getStyleValue(view.style.width)).toBe(100);
253
+
254
+ setHasBackground!(false);
255
+ await flushMicrotasks();
256
+
257
+ expect(view.style.backgroundColor).toBeUndefined();
258
+ expect(getStyleValue(view.style.width)).toBe(100);
259
+ });
260
+ });
261
+
262
+ describe('React effects', () => {
263
+ it('runs useEffect after render', async () => {
264
+ const container = createMockContainer();
265
+ const effectFn = vi.fn();
266
+
267
+ function EffectComponent() {
268
+ useEffect(() => {
269
+ effectFn();
270
+ }, []);
271
+ return <ojs-view />;
272
+ }
273
+
274
+ render(<EffectComponent />, container as any);
275
+ await flushMicrotasks();
276
+
277
+ expect(effectFn).toHaveBeenCalled();
278
+ });
279
+
280
+ it('runs cleanup on unmount', async () => {
281
+ const container = createMockContainer();
282
+ const cleanupFn = vi.fn();
283
+
284
+ function CleanupComponent() {
285
+ useEffect(() => {
286
+ return cleanupFn;
287
+ }, []);
288
+ return <ojs-view />;
289
+ }
290
+
291
+ render(<CleanupComponent />, container as any);
292
+ await flushMicrotasks();
293
+
294
+ expect(cleanupFn).not.toHaveBeenCalled();
295
+
296
+ unmount(container as any);
297
+ await flushMicrotasks();
298
+
299
+ expect(cleanupFn).toHaveBeenCalled();
300
+ });
301
+ });
302
+
303
+ describe('conditional rendering', () => {
304
+ it('adds and removes children based on condition', async () => {
305
+ const container = createMockContainer();
306
+ let setShowChild: (b: boolean) => void;
307
+
308
+ function ConditionalChild() {
309
+ const [showChild, _setShowChild] = useState(true);
310
+ setShowChild = _setShowChild;
311
+ return (
312
+ <ojs-view>
313
+ {showChild && <ojs-label text="Child" />}
314
+ </ojs-view>
315
+ );
316
+ }
317
+
318
+ render(<ConditionalChild />, container as any);
319
+ await flushMicrotasks();
320
+
321
+ const view = container.children[0] as MockVisualElement;
322
+ expect(view.childCount).toBe(1);
323
+
324
+ setShowChild!(false);
325
+ await flushMicrotasks();
326
+
327
+ expect(view.childCount).toBe(0);
328
+
329
+ setShowChild!(true);
330
+ await flushMicrotasks();
331
+
332
+ expect(view.childCount).toBe(1);
333
+ });
334
+ });
335
+
336
+ describe('lists', () => {
337
+ it('renders list of elements', async () => {
338
+ const container = createMockContainer();
339
+ const items = ['A', 'B', 'C'];
340
+
341
+ render(
342
+ <ojs-view>
343
+ {items.map((item, i) => (
344
+ <ojs-label key={i} text={item} />
345
+ ))}
346
+ </ojs-view>,
347
+ container as any
348
+ );
349
+ await flushMicrotasks();
350
+
351
+ const view = container.children[0] as MockVisualElement;
352
+ expect(view.childCount).toBe(3);
353
+ expect((view.children[0] as MockVisualElement).text).toBe('A');
354
+ expect((view.children[1] as MockVisualElement).text).toBe('B');
355
+ expect((view.children[2] as MockVisualElement).text).toBe('C');
356
+ });
357
+
358
+ it('updates list when items change', async () => {
359
+ const container = createMockContainer();
360
+ let setItems: (items: string[]) => void;
361
+
362
+ function List() {
363
+ const [items, _setItems] = useState(['A', 'B']);
364
+ setItems = _setItems;
365
+ return (
366
+ <ojs-view>
367
+ {items.map((item) => (
368
+ <ojs-label key={item} text={item} />
369
+ ))}
370
+ </ojs-view>
371
+ );
372
+ }
373
+
374
+ render(<List />, container as any);
375
+ await flushMicrotasks();
376
+
377
+ const view = container.children[0] as MockVisualElement;
378
+ expect(view.childCount).toBe(2);
379
+
380
+ setItems!(['A', 'B', 'C']);
381
+ await flushMicrotasks();
382
+
383
+ expect(view.childCount).toBe(3);
384
+ expect((view.children[2] as MockVisualElement).text).toBe('C');
385
+ });
386
+ });
387
+ });