Skip to content

Commit e76d014

Browse files
authored
test: raise coverage above 95% floor across profile, clients and whats-on (#620)
* test: raise coverage above 95% floor across profile, clients and whats-on * test: restructure 4 profile specs to DCL when/should standard * test: restructure ProfileLayout spec to DCL when/should standard * test: fix ProfileTabs.types spec tab type to unblock build
1 parent aadcb7c commit e76d014

42 files changed

Lines changed: 4934 additions & 203 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

jest.config.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ const jestConfig: Config = {
1010
'ts-jest',
1111
{
1212
tsconfig: {
13+
// Match the app build target (tsconfig.app.json) so native ES2020
14+
// features survive transpilation. Without this, ts-jest down-levels
15+
// the `**` operator to `Math.pow`, which throws on BigInt operands
16+
// (e.g. `10n ** 18n` in OverviewTab.helpers' `formatPriceMana`).
17+
target: 'ES2020',
1318
esModuleInterop: true,
1419
jsx: 'react-jsx'
1520
},

src/components/Layout/DownloadLayout.spec.tsx

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,37 @@ describe('DownloadLayout', () => {
123123
await userEvent.click(screen.getByText('component.landing.navbar.sign_in'))
124124
expect(mockRedirectToAuth).toHaveBeenCalledTimes(1)
125125
})
126+
127+
it('should navigate to decentraland.org when the logo is clicked', async () => {
128+
const assignSpy = jest.fn()
129+
const original = window.location
130+
// Redefine location so the assignment in the click handler is observable
131+
// and does not trigger jsdom's "navigation not implemented" warning.
132+
Object.defineProperty(window, 'location', {
133+
configurable: true,
134+
value: {
135+
...original,
136+
set href(value: string) {
137+
assignSpy(value)
138+
},
139+
get href() {
140+
return original.href
141+
}
142+
}
143+
})
144+
try {
145+
render(<DownloadLayout title={TITLE} />)
146+
// DclLogo is the label-less clickable element rendered immediately before
147+
// the Sign In button in the signed-out chrome.
148+
const signIn = screen.getByText('component.landing.navbar.sign_in')
149+
const logo = signIn.previousElementSibling as HTMLElement
150+
expect(logo).not.toBeNull()
151+
await userEvent.click(logo)
152+
expect(assignSpy).toHaveBeenCalledWith('https://decentraland.org')
153+
} finally {
154+
Object.defineProperty(window, 'location', { configurable: true, value: original })
155+
}
156+
})
126157
})
127158

128159
describe('when the user is signed in', () => {
@@ -157,6 +188,63 @@ describe('DownloadLayout', () => {
157188
render(<DownloadLayout title={TITLE} />)
158189
await waitFor(() => expect(screen.getByTestId('wearable-preview')).toBeInTheDocument())
159190
})
191+
192+
it('should set an accessible title on the preview iframe once it is present in the container', async () => {
193+
// The ref callback wires the container; once the WearablePreview iframe is
194+
// rendered the effect's `existing` branch runs and labels it for a11y.
195+
mockUseInView.mockReturnValue({ ref: jest.fn(), inView: true } as unknown as ReturnType<typeof useInView>)
196+
render(<DownloadLayout title={TITLE} />)
197+
await waitFor(() => expect(screen.getByTestId('wearable-preview')).toHaveAttribute('title', 'page.download.avatar_preview'))
198+
})
199+
200+
it('should label an iframe that the WearablePreview injects asynchronously via MutationObserver', async () => {
201+
// Render the preview wrapper but with NO iframe yet, then inject one into
202+
// the container after mount so the MutationObserver branch (not the
203+
// synchronous `existing` branch) sets the title.
204+
// eslint-disable-next-line @typescript-eslint/no-require-imports
205+
const wpModule = require('decentraland-ui2/dist/components/WearablePreview/WearablePreview')
206+
const originalPreview = wpModule.WearablePreview
207+
// Replace the preview with an empty container so no iframe exists initially.
208+
wpModule.WearablePreview = () => <div data-testid="empty-preview" />
209+
try {
210+
mockUseInView.mockReturnValue({ ref: jest.fn(), inView: true } as unknown as ReturnType<typeof useInView>)
211+
const { container } = render(<DownloadLayout title={TITLE} />)
212+
await waitFor(() => expect(screen.getByTestId('empty-preview')).toBeInTheDocument())
213+
214+
const host = screen.getByTestId('empty-preview').parentElement as HTMLElement
215+
const injected = document.createElement('iframe')
216+
host.appendChild(injected)
217+
218+
await waitFor(() => expect(injected).toHaveAttribute('title', 'page.download.avatar_preview'))
219+
expect(container).toBeTruthy()
220+
} finally {
221+
wpModule.WearablePreview = originalPreview
222+
}
223+
})
224+
225+
it('should label an iframe nested inside an element the WearablePreview injects', async () => {
226+
// Same MutationObserver path, but the added node is a wrapper element that
227+
// contains the iframe rather than the iframe itself.
228+
// eslint-disable-next-line @typescript-eslint/no-require-imports
229+
const wpModule = require('decentraland-ui2/dist/components/WearablePreview/WearablePreview')
230+
const originalPreview = wpModule.WearablePreview
231+
wpModule.WearablePreview = () => <div data-testid="empty-preview-nested" />
232+
try {
233+
mockUseInView.mockReturnValue({ ref: jest.fn(), inView: true } as unknown as ReturnType<typeof useInView>)
234+
render(<DownloadLayout title={TITLE} />)
235+
await waitFor(() => expect(screen.getByTestId('empty-preview-nested')).toBeInTheDocument())
236+
237+
const host = screen.getByTestId('empty-preview-nested').parentElement as HTMLElement
238+
const wrapper = document.createElement('div')
239+
const nestedIframe = document.createElement('iframe')
240+
wrapper.appendChild(nestedIframe)
241+
host.appendChild(wrapper)
242+
243+
await waitFor(() => expect(nestedIframe).toHaveAttribute('title', 'page.download.avatar_preview'))
244+
} finally {
245+
wpModule.WearablePreview = originalPreview
246+
}
247+
})
160248
})
161249

162250
describe('when the Jump In link is clicked', () => {

src/components/Reels/Metadata/UserMetadata.spec.tsx

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,39 @@ describe('UserMetadata', () => {
8383
expect(trackMock).toHaveBeenCalledWith('Reels Show Wearables', { userAddress: '0xa' })
8484
})
8585

86+
it('should track the click, prevent default and open the profile modal on a plain left-click', () => {
87+
render(<UserMetadata user={baseUser} isFirst={true} />)
88+
const link = screen.getByText('alice').closest('a') as HTMLAnchorElement
89+
90+
const prevented = !fireEvent.click(link, { button: 0 })
91+
92+
expect(trackMock).toHaveBeenCalledWith('Reels Click Profile', {
93+
userAddress: '0xa',
94+
legacyProfileUrl: 'https://profile/0xa'
95+
})
96+
expect(openProfileMock).toHaveBeenCalledWith('0xa')
97+
expect(prevented).toBe(true)
98+
})
99+
100+
it('should let modifier clicks fall through so the profile can open in a new tab', () => {
101+
render(<UserMetadata user={baseUser} isFirst={true} />)
102+
const link = screen.getByText('alice').closest('a') as HTMLAnchorElement
103+
104+
fireEvent.click(link, { button: 0, metaKey: true })
105+
106+
// Tracking still fires, but navigation is left to the browser.
107+
expect(trackMock).toHaveBeenCalledWith('Reels Click Profile', {
108+
userAddress: '0xa',
109+
legacyProfileUrl: 'https://profile/0xa'
110+
})
111+
expect(openProfileMock).not.toHaveBeenCalled()
112+
})
113+
114+
it('should render a static name without a link when the user has no address', () => {
115+
render(<UserMetadata user={{ ...baseUser, userAddress: '' }} isFirst={true} />)
116+
expect(screen.getByText('alice').closest('a')).toBeNull()
117+
})
118+
86119
it('should render NoWearables placeholder when wearablesParsed is empty', () => {
87120
render(<UserMetadata user={baseUser} isFirst={true} initialWearableVisibility={true} />)
88121
expect(screen.getByText('component.reels.metadata.no_wearable')).toBeInTheDocument()
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { createElement } from 'react'
2+
import { render } from '@testing-library/react'
3+
import { EditButton, EditButtonIcon } from './EditProfileButton.styled'
4+
5+
// Drive the real `styled()` style callbacks through the shared shim so the
6+
// `EditButton`/`EditButtonIcon` definition bodies (including the responsive
7+
// `breakpoints.down('md')` override) execute for coverage.
8+
jest.mock('decentraland-ui2', () => jest.requireActual('../../../__test-utils__/styledMock'))
9+
10+
describe('EditProfileButton.styled', () => {
11+
it('should render the edit CTA styled button forwarding its children', () => {
12+
const { getByText } = render(createElement(EditButton, {}, 'Edit'))
13+
14+
expect(getByText('Edit')).toBeInTheDocument()
15+
})
16+
17+
it('should render the pencil icon slot as an inline-flex span wrapper', () => {
18+
const { getByText } = render(createElement(EditButtonIcon, null, '✎'))
19+
20+
expect(getByText('✎')).toBeInTheDocument()
21+
})
22+
})
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { getFriendButtonConfig } from './ProfileHeader.helpers'
2+
import type { FriendButtonConfig } from './ProfileHeader.helpers'
3+
4+
describe('when getting the friend button config', () => {
5+
let config: FriendButtonConfig
6+
7+
afterEach(() => {
8+
jest.resetAllMocks()
9+
})
10+
11+
describe('and a friend request has been sent', () => {
12+
beforeEach(() => {
13+
config = getFriendButtonConfig('request_sent')
14+
})
15+
16+
it('should offer the cancel action', () => {
17+
expect(config.action).toBe('cancel')
18+
})
19+
20+
it('should use the request-sent label', () => {
21+
expect(config.labelKey).toBe('profile.header.request_sent')
22+
})
23+
24+
it('should expose an icon', () => {
25+
expect(config.icon).toBeDefined()
26+
})
27+
})
28+
29+
describe('and a friend request has been received', () => {
30+
beforeEach(() => {
31+
config = getFriendButtonConfig('request_received')
32+
})
33+
34+
it('should offer the accept action', () => {
35+
expect(config.action).toBe('accept')
36+
})
37+
38+
it('should use the add-friend label', () => {
39+
expect(config.labelKey).toBe('profile.header.add_friend')
40+
})
41+
})
42+
43+
describe('and the friendship is accepted', () => {
44+
beforeEach(() => {
45+
config = getFriendButtonConfig('accepted')
46+
})
47+
48+
it('should offer the remove action', () => {
49+
expect(config.action).toBe('remove')
50+
})
51+
52+
it('should use the remove-friend label', () => {
53+
expect(config.labelKey).toBe('profile.header.remove_friend')
54+
})
55+
})
56+
57+
describe('and there is no relationship yet', () => {
58+
beforeEach(() => {
59+
config = getFriendButtonConfig('none')
60+
})
61+
62+
it('should default to the request action', () => {
63+
expect(config.action).toBe('request')
64+
})
65+
66+
it('should use the add-friend label', () => {
67+
expect(config.labelKey).toBe('profile.header.add_friend')
68+
})
69+
})
70+
71+
describe('and the user is blocked', () => {
72+
beforeEach(() => {
73+
config = getFriendButtonConfig('blocked')
74+
})
75+
76+
it('should fall through to the request action', () => {
77+
expect(config.action).toBe('request')
78+
})
79+
80+
it('should use the add-friend label', () => {
81+
expect(config.labelKey).toBe('profile.header.add_friend')
82+
})
83+
})
84+
85+
describe('and the status is undefined', () => {
86+
beforeEach(() => {
87+
config = getFriendButtonConfig(undefined)
88+
})
89+
90+
it('should fall through to the request action', () => {
91+
expect(config.action).toBe('request')
92+
})
93+
94+
it('should use the add-friend label', () => {
95+
expect(config.labelKey).toBe('profile.header.add_friend')
96+
})
97+
})
98+
})

0 commit comments

Comments
 (0)