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
|
@@ -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
|
+
});
|