8 changed files with 1464 additions and 59 deletions
@ -0,0 +1,170 @@ |
|||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest' |
||||||
|
import { render, waitFor } from '@testing-library/react' |
||||||
|
import UserAvatar from './index' |
||||||
|
import * as useFetchProfileHook from '@/hooks/useFetchProfile' |
||||||
|
|
||||||
|
// Mock the hooks and dependencies
|
||||||
|
vi.mock('@/hooks/useFetchProfile', () => ({ |
||||||
|
useFetchProfile: vi.fn() |
||||||
|
})) |
||||||
|
|
||||||
|
vi.mock('@/PageManager', () => ({ |
||||||
|
useSmartProfileNavigation: () => ({ |
||||||
|
navigateToProfile: vi.fn() |
||||||
|
}) |
||||||
|
})) |
||||||
|
|
||||||
|
vi.mock('@/lib/pubkey', () => ({ |
||||||
|
userIdToPubkey: (id: string) => id.startsWith('npub') ? 'decoded_pubkey' : id, |
||||||
|
generateImageByPubkey: (pubkey: string) => `https://avatar.example.com/${pubkey}` |
||||||
|
})) |
||||||
|
|
||||||
|
vi.mock('@/lib/link', () => ({ |
||||||
|
toProfile: (pubkey: string) => `/profile/${pubkey}` |
||||||
|
})) |
||||||
|
|
||||||
|
describe('UserAvatar in Embedded Notes', () => { |
||||||
|
beforeEach(() => { |
||||||
|
vi.clearAllMocks() |
||||||
|
}) |
||||||
|
|
||||||
|
it('should render avatar image fully visible without being covered', async () => { |
||||||
|
// Mock profile with avatar
|
||||||
|
vi.mocked(useFetchProfileHook.useFetchProfile).mockReturnValue({ |
||||||
|
isFetching: false, |
||||||
|
error: null, |
||||||
|
profile: { |
||||||
|
pubkey: 'test_pubkey', |
||||||
|
npub: 'npub_test', |
||||||
|
username: 'testuser', |
||||||
|
avatar: 'https://example.com/avatar.jpg' |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
const { container } = render( |
||||||
|
<div data-embedded-note> |
||||||
|
<div className="p-2 sm:p-3 border rounded-lg"> |
||||||
|
<div className="relative"> |
||||||
|
<UserAvatar userId="test_pubkey" size="medium" /> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
) |
||||||
|
|
||||||
|
// Find the avatar container
|
||||||
|
const avatarContainer = container.querySelector('[data-user-avatar]') |
||||||
|
expect(avatarContainer).toBeInTheDocument() |
||||||
|
|
||||||
|
// Find the image
|
||||||
|
const img = avatarContainer?.querySelector('img') |
||||||
|
expect(img).toBeInTheDocument() |
||||||
|
expect(img).toHaveAttribute('src', 'https://example.com/avatar.jpg') |
||||||
|
|
||||||
|
// Check that the image is not hidden or covered
|
||||||
|
const computedStyle = window.getComputedStyle(img!) |
||||||
|
expect(computedStyle.display).not.toBe('none') |
||||||
|
expect(computedStyle.visibility).not.toBe('hidden') |
||||||
|
expect(computedStyle.opacity).not.toBe('0') |
||||||
|
|
||||||
|
// Check that the container has overflow-hidden for rounded corners
|
||||||
|
// Note: In test environment, computed styles may not reflect Tailwind classes
|
||||||
|
// So we check the className instead
|
||||||
|
expect(avatarContainer?.className).toContain('overflow-hidden') |
||||||
|
|
||||||
|
// Simulate image load to remove loading placeholder
|
||||||
|
if (img) { |
||||||
|
img.dispatchEvent(new Event('load')) |
||||||
|
} |
||||||
|
|
||||||
|
// Wait for React to update state and remove loading placeholder
|
||||||
|
await waitFor(() => { |
||||||
|
const loadingPlaceholders = avatarContainer?.querySelectorAll('[class*="animate-pulse"]') |
||||||
|
expect(loadingPlaceholders?.length || 0).toBe(0) |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
it('should render avatar without loading placeholder covering it', async () => { |
||||||
|
vi.mocked(useFetchProfileHook.useFetchProfile).mockReturnValue({ |
||||||
|
isFetching: false, |
||||||
|
error: null, |
||||||
|
profile: { |
||||||
|
pubkey: 'test_pubkey', |
||||||
|
npub: 'npub_test', |
||||||
|
username: 'testuser', |
||||||
|
avatar: 'https://example.com/avatar.jpg' |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
const { container } = render( |
||||||
|
<div data-embedded-note> |
||||||
|
<div className="p-2 sm:p-3 border rounded-lg"> |
||||||
|
<UserAvatar userId="test_pubkey" size="medium" /> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
) |
||||||
|
|
||||||
|
const avatarContainer = container.querySelector('[data-user-avatar]') |
||||||
|
|
||||||
|
// Check that the image exists
|
||||||
|
const img = avatarContainer?.querySelector('img') |
||||||
|
expect(img).toBeInTheDocument() |
||||||
|
|
||||||
|
// Simulate image load to trigger removal of loading placeholder
|
||||||
|
if (img) { |
||||||
|
img.dispatchEvent(new Event('load')) |
||||||
|
} |
||||||
|
|
||||||
|
// Wait for React to update state and remove loading placeholder
|
||||||
|
await waitFor(() => { |
||||||
|
const loadingPlaceholders = avatarContainer?.querySelectorAll('[class*="animate-pulse"]') |
||||||
|
expect(loadingPlaceholders?.length || 0).toBe(0) |
||||||
|
}) |
||||||
|
|
||||||
|
// Check image is visible after loading
|
||||||
|
const imgStyle = window.getComputedStyle(img!) |
||||||
|
expect(imgStyle.display).not.toBe('none') |
||||||
|
expect(imgStyle.visibility).not.toBe('hidden') |
||||||
|
expect(imgStyle.opacity).not.toBe('0') |
||||||
|
}) |
||||||
|
|
||||||
|
it('should have correct z-index and positioning to prevent being covered', () => { |
||||||
|
vi.mocked(useFetchProfileHook.useFetchProfile).mockReturnValue({ |
||||||
|
isFetching: false, |
||||||
|
error: null, |
||||||
|
profile: { |
||||||
|
pubkey: 'test_pubkey', |
||||||
|
npub: 'npub_test', |
||||||
|
username: 'testuser', |
||||||
|
avatar: 'https://example.com/avatar.jpg' |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
const { container } = render( |
||||||
|
<div data-embedded-note> |
||||||
|
<div className="p-2 sm:p-3 border rounded-lg"> |
||||||
|
<div className="flex items-center space-x-2"> |
||||||
|
<UserAvatar userId="test_pubkey" size="medium" /> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
) |
||||||
|
|
||||||
|
const avatarContainer = container.querySelector('[data-user-avatar]') |
||||||
|
const computedStyle = window.getComputedStyle(avatarContainer!) |
||||||
|
|
||||||
|
// The container should have relative positioning (or static which is default)
|
||||||
|
// In CSS, if position is not set, it defaults to static
|
||||||
|
expect(['relative', 'static', '']).toContain(computedStyle.position) |
||||||
|
|
||||||
|
// Check that display is block or inline-block (not inline which could cause issues)
|
||||||
|
expect(['block', 'inline-block', 'flex']).toContain(computedStyle.display) |
||||||
|
|
||||||
|
// Most importantly: check that the image is visible and not covered
|
||||||
|
const img = avatarContainer?.querySelector('img') |
||||||
|
expect(img).toBeInTheDocument() |
||||||
|
const imgStyle = window.getComputedStyle(img!) |
||||||
|
expect(imgStyle.opacity).not.toBe('0') |
||||||
|
expect(imgStyle.display).not.toBe('none') |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
@ -0,0 +1,44 @@ |
|||||||
|
import '@testing-library/jest-dom' |
||||||
|
import { afterEach, vi } from 'vitest' |
||||||
|
import { cleanup } from '@testing-library/react' |
||||||
|
|
||||||
|
// Mock IndexedDB before any modules are loaded
|
||||||
|
// This needs to be set up synchronously, not in beforeAll
|
||||||
|
if (typeof globalThis.indexedDB === 'undefined') { |
||||||
|
globalThis.indexedDB = { |
||||||
|
open: vi.fn(() => { |
||||||
|
const request: any = { |
||||||
|
onerror: null, |
||||||
|
onsuccess: null, |
||||||
|
onupgradeneeded: null, |
||||||
|
result: { |
||||||
|
createObjectStore: vi.fn(), |
||||||
|
transaction: { |
||||||
|
objectStore: vi.fn(() => ({ |
||||||
|
add: vi.fn(), |
||||||
|
get: vi.fn(), |
||||||
|
put: vi.fn(), |
||||||
|
delete: vi.fn(), |
||||||
|
clear: vi.fn() |
||||||
|
})) |
||||||
|
} |
||||||
|
}, |
||||||
|
addEventListener: vi.fn(), |
||||||
|
removeEventListener: vi.fn() |
||||||
|
} |
||||||
|
// Simulate immediate success
|
||||||
|
setTimeout(() => { |
||||||
|
if (request.onsuccess) { |
||||||
|
request.onsuccess({} as any) |
||||||
|
} |
||||||
|
}, 0) |
||||||
|
return request |
||||||
|
}) |
||||||
|
} as any |
||||||
|
} |
||||||
|
|
||||||
|
// Cleanup after each test
|
||||||
|
afterEach(() => { |
||||||
|
cleanup() |
||||||
|
}) |
||||||
|
|
||||||
Loading…
Reference in new issue