Skip to content

Commit 3eab4dc

Browse files
authored
fix: render the profile avatar with the unity preview engine (#635)
1 parent c6fb254 commit 3eab4dc

2 files changed

Lines changed: 114 additions & 0 deletions

File tree

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { act, render, screen } from '@testing-library/react'
2+
import { PreviewUnityMode } from '@dcl/schemas'
3+
import { AvatarRender } from './AvatarRender'
4+
5+
const mockWearablePreviewProps = jest.fn()
6+
const mockUseProfileAvatar = jest.fn()
7+
const mockGetEnv = jest.fn()
8+
9+
jest.mock('decentraland-ui2', () => {
10+
const styledMock = jest.requireActual('../../../__test-utils__/styledMock')
11+
return {
12+
...styledMock,
13+
WearablePreview: (props: Record<string, unknown>) => {
14+
mockWearablePreviewProps(props)
15+
return <div data-testid="wearable-preview" />
16+
},
17+
CircularProgress: () => <div data-testid="avatar-loader" />
18+
}
19+
})
20+
21+
jest.mock('../../../config/env', () => ({
22+
getEnv: (key: string) => mockGetEnv(key)
23+
}))
24+
25+
jest.mock('../../../hooks/useProfileAvatar', () => ({
26+
useProfileAvatar: (address: string | undefined) => mockUseProfileAvatar(address)
27+
}))
28+
29+
const ADDRESS = '0xd9B96B5dC720fC52BedE1EC3B40A930e15F70Ddd'
30+
const BODY_SNAPSHOT = 'https://peer.decentraland.org/content/contents/body.png'
31+
32+
function lastPreviewProps(): Record<string, unknown> {
33+
return mockWearablePreviewProps.mock.calls.at(-1)?.[0] ?? {}
34+
}
35+
36+
describe('AvatarRender', () => {
37+
beforeEach(() => {
38+
mockGetEnv.mockReturnValue('https://wearable-preview.decentraland.org/')
39+
mockUseProfileAvatar.mockReturnValue({
40+
avatar: { avatar: { snapshots: { body: BODY_SNAPSHOT } } },
41+
backgroundColor: '#ff2d55'
42+
})
43+
})
44+
45+
afterEach(() => {
46+
jest.resetAllMocks()
47+
})
48+
49+
describe('when rendering the preview', () => {
50+
it('should enable the Unity renderer so it matches the Marketplace preview', () => {
51+
render(<AvatarRender address={ADDRESS} />)
52+
expect(lastPreviewProps()).toEqual(expect.objectContaining({ unity: true }))
53+
})
54+
55+
it('should render the preview in the profile Unity mode', () => {
56+
render(<AvatarRender address={ADDRESS} />)
57+
expect(lastPreviewProps()).toEqual(expect.objectContaining({ unityMode: PreviewUnityMode.PROFILE }))
58+
})
59+
60+
it('should drive the preview off the lowercased address', () => {
61+
render(<AvatarRender address={ADDRESS} />)
62+
expect(lastPreviewProps()).toEqual(expect.objectContaining({ profile: ADDRESS.toLowerCase() }))
63+
})
64+
65+
it('should resolve the base url from the WEARABLE_PREVIEW_URL env key, stripping the trailing slash', () => {
66+
render(<AvatarRender address={ADDRESS} />)
67+
expect(mockGetEnv).toHaveBeenCalledWith('WEARABLE_PREVIEW_URL')
68+
expect(lastPreviewProps()).toEqual(expect.objectContaining({ baseUrl: 'https://wearable-preview.decentraland.org' }))
69+
})
70+
71+
it('should show the loader until the preview reports it has loaded', () => {
72+
render(<AvatarRender address={ADDRESS} />)
73+
expect(screen.getByTestId('avatar-loader')).toBeInTheDocument()
74+
75+
act(() => {
76+
;(lastPreviewProps().onLoad as () => void)()
77+
})
78+
79+
expect(screen.queryByTestId('avatar-loader')).not.toBeInTheDocument()
80+
})
81+
})
82+
83+
describe('when the preview fails to load', () => {
84+
it('should fall back to the body snapshot image', () => {
85+
render(<AvatarRender address={ADDRESS} />)
86+
87+
act(() => {
88+
;(lastPreviewProps().onError as (error: Error) => void)(new Error('boom'))
89+
})
90+
91+
expect(screen.queryByTestId('wearable-preview')).not.toBeInTheDocument()
92+
expect(screen.getByAltText('Avatar')).toHaveAttribute('src', BODY_SNAPSHOT)
93+
})
94+
95+
it('should render nothing extra when there is no body snapshot to fall back to', () => {
96+
mockUseProfileAvatar.mockReturnValue({ avatar: undefined, backgroundColor: '#ff2d55' })
97+
render(<AvatarRender address={ADDRESS} />)
98+
99+
act(() => {
100+
;(lastPreviewProps().onError as (error: Error) => void)(new Error('boom'))
101+
})
102+
103+
expect(screen.queryByTestId('wearable-preview')).not.toBeInTheDocument()
104+
expect(screen.queryByAltText('Avatar')).not.toBeInTheDocument()
105+
})
106+
})
107+
})

src/components/profile/AvatarRender/AvatarRender.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { useCallback, useState } from 'react'
2+
import { PreviewUnityMode } from '@dcl/schemas'
23
import { CircularProgress, WearablePreview } from 'decentraland-ui2'
34
import { getEnv } from '../../../config/env'
45
import { useProfileAvatar } from '../../../hooks/useProfileAvatar'
@@ -45,6 +46,12 @@ function AvatarRender({ address }: AvatarRenderProps) {
4546
<WearablePreview
4647
baseUrl={baseUrl}
4748
profile={address.toLowerCase()}
49+
// Render with the Unity engine (matching the Marketplace avatar preview) for the
50+
// high-fidelity lighting/materials. `unity` is the actual renderer switch: without
51+
// it the iframe falls back to the lower-quality Babylon renderer even when
52+
// `unityMode` is set. `PROFILE` is the Unity layout tuned for profile avatars.
53+
unity
54+
unityMode={PreviewUnityMode.PROFILE}
4855
disableBackground
4956
disableDefaultWearables
5057
lockBeta

0 commit comments

Comments
 (0)