8 changed files with 1464 additions and 59 deletions
@ -0,0 +1,170 @@
@@ -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 @@
@@ -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