Skip to content

Commit 95189a7

Browse files
authored
chore: virtual screen test coverage (#1319)
1 parent ce67806 commit 95189a7

File tree

1 file changed

+355
-0
lines changed

1 file changed

+355
-0
lines changed
Lines changed: 355 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,355 @@
1+
import { Entity } from '../../packages/@dcl/ecs/src/engine'
2+
import { YGUnit } from '../../packages/@dcl/ecs'
3+
import { components } from '../../packages/@dcl/ecs/src'
4+
import { ReactEcs, UiEntity } from '../../packages/@dcl/react-ecs/src'
5+
import {
6+
getUiScaleFactor,
7+
resetUiScaleFactor
8+
} from '../../packages/@dcl/react-ecs/src/components/utils'
9+
import { setupEngine } from './utils'
10+
11+
function Panel1() {
12+
return <UiEntity uiTransform={{ width: 300, height: 300 }} />
13+
}
14+
15+
function Panel2() {
16+
return <UiEntity uiTransform={{ width: 600, height: 400 }} />
17+
}
18+
19+
// Parent uses % — children use pixels
20+
function PercentParentWithPixelChildren() {
21+
return (
22+
<UiEntity uiTransform={{ width: '100%', height: '50%' }}>
23+
<UiEntity uiTransform={{ width: 200, height: 100 }} />
24+
<UiEntity uiTransform={{ width: '150px', height: '80px' }} />
25+
</UiEntity>
26+
)
27+
}
28+
29+
// Parent uses pixels — children use %
30+
function PixelParentWithPercentChildren() {
31+
return (
32+
<UiEntity uiTransform={{ width: 400, height: 300 }}>
33+
<UiEntity uiTransform={{ width: '50%', height: '25%' }} />
34+
<UiEntity uiTransform={{ width: 100, height: 50 }} />
35+
</UiEntity>
36+
)
37+
}
38+
39+
// Deep nesting with mixed units
40+
function DeeplyNestedMixed() {
41+
return (
42+
<UiEntity uiTransform={{ width: '100%', height: '100%' }}>
43+
<UiEntity uiTransform={{ width: 500, height: 250, padding: 10 }}>
44+
<UiEntity uiTransform={{ width: '50%', height: '50%' }}>
45+
<UiEntity uiTransform={{ width: 100, height: 60, margin: '20px' }} />
46+
</UiEntity>
47+
</UiEntity>
48+
</UiEntity>
49+
)
50+
}
51+
52+
function createCanvasInfo(engine: any) {
53+
const UiCanvasInformation = components.UiCanvasInformation(engine)
54+
UiCanvasInformation.create(engine.RootEntity, {
55+
devicePixelRatio: 1,
56+
width: 3840,
57+
height: 2160,
58+
interactableArea: { left: 0, right: 0, top: 0, bottom: 0 }
59+
})
60+
return UiCanvasInformation
61+
}
62+
63+
describe('Virtual scale factor with array UI renderers', () => {
64+
afterEach(() => {
65+
resetUiScaleFactor()
66+
})
67+
68+
it('should apply scale factor when returning a single element', async () => {
69+
const { engine, uiRenderer } = setupEngine()
70+
const UiTransform = components.UiTransform(engine)
71+
createCanvasInfo(engine)
72+
const entityIndex = engine.addEntity() as number
73+
74+
uiRenderer.setUiRenderer(
75+
() => <Panel1 />,
76+
{ virtualWidth: 1920, virtualHeight: 1080 }
77+
)
78+
79+
await engine.update(1)
80+
81+
// scale = Math.min(3840/1920, 2160/1080) = 2
82+
expect(getUiScaleFactor()).toBe(2)
83+
84+
const panelEntity = (entityIndex + 1) as Entity
85+
expect(UiTransform.get(panelEntity).width).toBe(600) // 300 * 2
86+
expect(UiTransform.get(panelEntity).height).toBe(600) // 300 * 2
87+
88+
uiRenderer.destroy()
89+
})
90+
91+
it('should apply scale factor when returning an array of direct function calls', async () => {
92+
const { engine, uiRenderer } = setupEngine()
93+
const UiTransform = components.UiTransform(engine)
94+
createCanvasInfo(engine)
95+
const entityIndex = engine.addEntity() as number
96+
97+
uiRenderer.setUiRenderer(
98+
() => [Panel1(), Panel2()],
99+
{ virtualWidth: 1920, virtualHeight: 1080 }
100+
)
101+
102+
await engine.update(1)
103+
104+
expect(getUiScaleFactor()).toBe(2)
105+
106+
const panel1Entity = (entityIndex + 1) as Entity
107+
const panel2Entity = (entityIndex + 2) as Entity
108+
expect(UiTransform.get(panel1Entity).width).toBe(600) // 300 * 2
109+
expect(UiTransform.get(panel1Entity).height).toBe(600) // 300 * 2
110+
expect(UiTransform.get(panel2Entity).width).toBe(1200) // 600 * 2
111+
expect(UiTransform.get(panel2Entity).height).toBe(800) // 400 * 2
112+
113+
uiRenderer.destroy()
114+
})
115+
116+
it('should apply scale factor when returning an array of JSX components', async () => {
117+
const { engine, uiRenderer } = setupEngine()
118+
const UiTransform = components.UiTransform(engine)
119+
createCanvasInfo(engine)
120+
const entityIndex = engine.addEntity() as number
121+
122+
uiRenderer.setUiRenderer(
123+
() => [<Panel1 key="p1" />, <Panel2 key="p2" />],
124+
{ virtualWidth: 1920, virtualHeight: 1080 }
125+
)
126+
127+
await engine.update(1)
128+
129+
expect(getUiScaleFactor()).toBe(2)
130+
131+
const panel1Entity = (entityIndex + 1) as Entity
132+
const panel2Entity = (entityIndex + 2) as Entity
133+
expect(UiTransform.get(panel1Entity).width).toBe(600)
134+
expect(UiTransform.get(panel1Entity).height).toBe(600)
135+
expect(UiTransform.get(panel2Entity).width).toBe(1200)
136+
expect(UiTransform.get(panel2Entity).height).toBe(800)
137+
138+
uiRenderer.destroy()
139+
})
140+
})
141+
142+
describe('Virtual scale factor with mixed % and pixel values', () => {
143+
afterEach(() => {
144+
resetUiScaleFactor()
145+
})
146+
147+
it('should scale pixel children inside a %-sized parent', async () => {
148+
const { engine, uiRenderer } = setupEngine()
149+
const UiTransform = components.UiTransform(engine)
150+
createCanvasInfo(engine)
151+
const entityIndex = engine.addEntity() as number
152+
153+
// scale = Math.min(3840/1920, 2160/1080) = 2
154+
uiRenderer.setUiRenderer(
155+
() => <PercentParentWithPixelChildren />,
156+
{ virtualWidth: 1920, virtualHeight: 1080 }
157+
)
158+
159+
await engine.update(1)
160+
expect(getUiScaleFactor()).toBe(2)
161+
162+
// React reconciler creates entities bottom-up (children before parents)
163+
// Child 1: 200px, 100px — pixels should be scaled by 2
164+
const child1Entity = (entityIndex + 1) as Entity
165+
expect(UiTransform.get(child1Entity).width).toBe(400) // 200 * 2
166+
expect(UiTransform.get(child1Entity).widthUnit).toBe(YGUnit.YGU_POINT)
167+
expect(UiTransform.get(child1Entity).height).toBe(200) // 100 * 2
168+
expect(UiTransform.get(child1Entity).heightUnit).toBe(YGUnit.YGU_POINT)
169+
170+
// Child 2: '150px', '80px' — px strings should also be scaled by 2
171+
const child2Entity = (entityIndex + 2) as Entity
172+
expect(UiTransform.get(child2Entity).width).toBe(300) // 150 * 2
173+
expect(UiTransform.get(child2Entity).widthUnit).toBe(YGUnit.YGU_POINT)
174+
expect(UiTransform.get(child2Entity).height).toBe(160) // 80 * 2
175+
expect(UiTransform.get(child2Entity).heightUnit).toBe(YGUnit.YGU_POINT)
176+
177+
// Parent: 100%, 50% — percentages should NOT be scaled
178+
const parentEntity = (entityIndex + 3) as Entity
179+
expect(UiTransform.get(parentEntity).width).toBe(100)
180+
expect(UiTransform.get(parentEntity).widthUnit).toBe(YGUnit.YGU_PERCENT)
181+
expect(UiTransform.get(parentEntity).height).toBe(50)
182+
expect(UiTransform.get(parentEntity).heightUnit).toBe(YGUnit.YGU_PERCENT)
183+
184+
uiRenderer.destroy()
185+
})
186+
187+
it('should scale pixel parent but not %-sized children', async () => {
188+
const { engine, uiRenderer } = setupEngine()
189+
const UiTransform = components.UiTransform(engine)
190+
createCanvasInfo(engine)
191+
const entityIndex = engine.addEntity() as number
192+
193+
uiRenderer.setUiRenderer(
194+
() => <PixelParentWithPercentChildren />,
195+
{ virtualWidth: 1920, virtualHeight: 1080 }
196+
)
197+
198+
await engine.update(1)
199+
expect(getUiScaleFactor()).toBe(2)
200+
201+
// React reconciler creates entities bottom-up (children before parents)
202+
// Child 1: 50%, 25% — NOT scaled
203+
const child1Entity = (entityIndex + 1) as Entity
204+
expect(UiTransform.get(child1Entity).width).toBe(50)
205+
expect(UiTransform.get(child1Entity).widthUnit).toBe(YGUnit.YGU_PERCENT)
206+
expect(UiTransform.get(child1Entity).height).toBe(25)
207+
expect(UiTransform.get(child1Entity).heightUnit).toBe(YGUnit.YGU_PERCENT)
208+
209+
// Child 2: 100px, 50px — scaled by 2
210+
const child2Entity = (entityIndex + 2) as Entity
211+
expect(UiTransform.get(child2Entity).width).toBe(200) // 100 * 2
212+
expect(UiTransform.get(child2Entity).widthUnit).toBe(YGUnit.YGU_POINT)
213+
expect(UiTransform.get(child2Entity).height).toBe(100) // 50 * 2
214+
expect(UiTransform.get(child2Entity).heightUnit).toBe(YGUnit.YGU_POINT)
215+
216+
// Parent: 400px, 300px — scaled by 2
217+
const parentEntity = (entityIndex + 3) as Entity
218+
expect(UiTransform.get(parentEntity).width).toBe(800) // 400 * 2
219+
expect(UiTransform.get(parentEntity).widthUnit).toBe(YGUnit.YGU_POINT)
220+
expect(UiTransform.get(parentEntity).height).toBe(600) // 300 * 2
221+
expect(UiTransform.get(parentEntity).heightUnit).toBe(YGUnit.YGU_POINT)
222+
223+
uiRenderer.destroy()
224+
})
225+
226+
it('should handle deeply nested mixed % and pixel values', async () => {
227+
const { engine, uiRenderer } = setupEngine()
228+
const UiTransform = components.UiTransform(engine)
229+
createCanvasInfo(engine)
230+
const entityIndex = engine.addEntity() as number
231+
232+
uiRenderer.setUiRenderer(
233+
() => <DeeplyNestedMixed />,
234+
{ virtualWidth: 1920, virtualHeight: 1080 }
235+
)
236+
237+
await engine.update(1)
238+
expect(getUiScaleFactor()).toBe(2)
239+
240+
// React reconciler creates entities bottom-up (deepest children first)
241+
// Level 3 (deepest): 100px, 60px, margin '20px' — all scaled by 2
242+
const level3 = (entityIndex + 1) as Entity
243+
expect(UiTransform.get(level3).width).toBe(200) // 100 * 2
244+
expect(UiTransform.get(level3).height).toBe(120) // 60 * 2
245+
expect(UiTransform.get(level3).marginTop).toBe(40) // 20 * 2
246+
expect(UiTransform.get(level3).marginBottom).toBe(40)
247+
expect(UiTransform.get(level3).marginLeft).toBe(40)
248+
expect(UiTransform.get(level3).marginRight).toBe(40)
249+
250+
// Level 2: 50%, 50% — NOT scaled
251+
const level2 = (entityIndex + 2) as Entity
252+
expect(UiTransform.get(level2).width).toBe(50)
253+
expect(UiTransform.get(level2).widthUnit).toBe(YGUnit.YGU_PERCENT)
254+
expect(UiTransform.get(level2).height).toBe(50)
255+
expect(UiTransform.get(level2).heightUnit).toBe(YGUnit.YGU_PERCENT)
256+
257+
// Level 1: 500px, 250px, padding 10px — all scaled by 2
258+
const level1 = (entityIndex + 3) as Entity
259+
expect(UiTransform.get(level1).width).toBe(1000) // 500 * 2
260+
expect(UiTransform.get(level1).height).toBe(500) // 250 * 2
261+
expect(UiTransform.get(level1).paddingTop).toBe(20) // 10 * 2
262+
expect(UiTransform.get(level1).paddingBottom).toBe(20)
263+
expect(UiTransform.get(level1).paddingLeft).toBe(20)
264+
expect(UiTransform.get(level1).paddingRight).toBe(20)
265+
266+
// Level 0 (root): 100%, 100% — NOT scaled
267+
const level0 = (entityIndex + 4) as Entity
268+
expect(UiTransform.get(level0).width).toBe(100)
269+
expect(UiTransform.get(level0).widthUnit).toBe(YGUnit.YGU_PERCENT)
270+
expect(UiTransform.get(level0).height).toBe(100)
271+
expect(UiTransform.get(level0).heightUnit).toBe(YGUnit.YGU_PERCENT)
272+
273+
uiRenderer.destroy()
274+
})
275+
276+
it('should scale pixel children inside %-sized parent returned as array', async () => {
277+
const { engine, uiRenderer } = setupEngine()
278+
const UiTransform = components.UiTransform(engine)
279+
createCanvasInfo(engine)
280+
const entityIndex = engine.addEntity() as number
281+
282+
// Combine the two scenarios: array of elements + mixed %/pixel values
283+
uiRenderer.setUiRenderer(
284+
() => [PercentParentWithPixelChildren(), PixelParentWithPercentChildren()],
285+
{ virtualWidth: 1920, virtualHeight: 1080 }
286+
)
287+
288+
await engine.update(1)
289+
expect(getUiScaleFactor()).toBe(2)
290+
291+
// React reconciler creates entities bottom-up per tree, trees processed left to right
292+
293+
// --- First tree: PercentParentWithPixelChildren ---
294+
// Child 1: 200, 100 → scaled by 2
295+
const tree1Child1 = (entityIndex + 1) as Entity
296+
expect(UiTransform.get(tree1Child1).width).toBe(400)
297+
expect(UiTransform.get(tree1Child1).height).toBe(200)
298+
299+
// Child 2: '150px', '80px' → scaled by 2
300+
const tree1Child2 = (entityIndex + 2) as Entity
301+
expect(UiTransform.get(tree1Child2).width).toBe(300)
302+
expect(UiTransform.get(tree1Child2).height).toBe(160)
303+
304+
// Parent: 100%, 50%
305+
const tree1Parent = (entityIndex + 3) as Entity
306+
expect(UiTransform.get(tree1Parent).width).toBe(100)
307+
expect(UiTransform.get(tree1Parent).widthUnit).toBe(YGUnit.YGU_PERCENT)
308+
expect(UiTransform.get(tree1Parent).height).toBe(50)
309+
expect(UiTransform.get(tree1Parent).heightUnit).toBe(YGUnit.YGU_PERCENT)
310+
311+
// --- Second tree: PixelParentWithPercentChildren ---
312+
// Child 1: 50%, 25% → NOT scaled
313+
const tree2Child1 = (entityIndex + 4) as Entity
314+
expect(UiTransform.get(tree2Child1).width).toBe(50)
315+
expect(UiTransform.get(tree2Child1).widthUnit).toBe(YGUnit.YGU_PERCENT)
316+
expect(UiTransform.get(tree2Child1).height).toBe(25)
317+
expect(UiTransform.get(tree2Child1).heightUnit).toBe(YGUnit.YGU_PERCENT)
318+
319+
// Child 2: 100, 50 → scaled by 2
320+
const tree2Child2 = (entityIndex + 5) as Entity
321+
expect(UiTransform.get(tree2Child2).width).toBe(200)
322+
expect(UiTransform.get(tree2Child2).height).toBe(100)
323+
324+
// Parent: 400, 300 → scaled by 2
325+
const tree2Parent = (entityIndex + 6) as Entity
326+
expect(UiTransform.get(tree2Parent).width).toBe(800)
327+
expect(UiTransform.get(tree2Parent).height).toBe(600)
328+
329+
uiRenderer.destroy()
330+
})
331+
332+
it('should use scale factor 1 when UiCanvasInformation is not yet available', async () => {
333+
const { engine, uiRenderer } = setupEngine()
334+
const UiTransform = components.UiTransform(engine)
335+
const entityIndex = engine.addEntity() as number
336+
337+
// Do NOT create UiCanvasInformation — simulates the first tick before the renderer provides it
338+
uiRenderer.setUiRenderer(
339+
() => <UiEntity uiTransform={{ width: 300, height: 200 }} />,
340+
{ virtualWidth: 1920, virtualHeight: 1080 }
341+
)
342+
343+
await engine.update(1)
344+
345+
// Scale factor should remain at the default of 1
346+
expect(getUiScaleFactor()).toBe(1)
347+
348+
// Pixel values should pass through unscaled
349+
const entity = (entityIndex + 1) as Entity
350+
expect(UiTransform.get(entity).width).toBe(300)
351+
expect(UiTransform.get(entity).height).toBe(200)
352+
353+
uiRenderer.destroy()
354+
})
355+
})

0 commit comments

Comments
 (0)