mirror of
https://github.com/utopia-os/utopia-ui.git
synced 2026-02-06 09:55:47 +00:00
test: add component tests for TipTap editor integration
- Add TextViewStatic.spec.tsx (16 tests): static HTML renderer tests - Add TextView.spec.tsx (10 tests): TipTap read-only viewer tests - Add RichTextEditor.spec.tsx (9 tests): editable editor tests - Update setupTest.ts with TipTap DOM mocks (Range, Document APIs) Tests cover rendering, truncation, hashtag/mention styling and clicks, link navigation, and video embed rendering.
This commit is contained in:
parent
8e5c6a0907
commit
942559247e
@ -1,2 +1,26 @@
|
||||
// eslint-disable-next-line import-x/no-unassigned-import
|
||||
import '@testing-library/jest-dom'
|
||||
import { vi } from 'vitest'
|
||||
|
||||
// TipTap requires Range and Document APIs that happy-dom doesn't fully implement
|
||||
Range.prototype.getBoundingClientRect = () => ({
|
||||
bottom: 0,
|
||||
height: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
width: 0,
|
||||
x: 0,
|
||||
y: 0,
|
||||
toJSON: vi.fn(),
|
||||
})
|
||||
|
||||
Range.prototype.getClientRects = () => ({
|
||||
item: () => null,
|
||||
length: 0,
|
||||
[Symbol.iterator]: vi.fn(),
|
||||
})
|
||||
|
||||
if (typeof Document.prototype.elementFromPoint === 'undefined') {
|
||||
Document.prototype.elementFromPoint = vi.fn()
|
||||
}
|
||||
|
||||
175
lib/src/Components/Input/RichTextEditor.spec.tsx
Normal file
175
lib/src/Components/Input/RichTextEditor.spec.tsx
Normal file
@ -0,0 +1,175 @@
|
||||
import { render, screen, waitFor, fireEvent } from '@testing-library/react'
|
||||
import { MemoryRouter } from 'react-router-dom'
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
|
||||
import { RichTextEditor } from './RichTextEditor'
|
||||
|
||||
import type { Item } from '#types/Item'
|
||||
import type { Tag } from '#types/Tag'
|
||||
|
||||
vi.mock('#components/Map/hooks/useTags')
|
||||
vi.mock('#components/Map/hooks/useItems')
|
||||
vi.mock('#components/Map/hooks/useItemColor')
|
||||
|
||||
const Wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<MemoryRouter>{children}</MemoryRouter>
|
||||
)
|
||||
|
||||
const mockTags: Tag[] = [
|
||||
{ id: '1', name: 'nature', color: '#22c55e' },
|
||||
{ id: '2', name: 'tech', color: '#3b82f6' },
|
||||
]
|
||||
|
||||
const mockItems: Item[] = [
|
||||
{ id: 'abc-123', name: 'Alice', color: '#ef4444' } as Item,
|
||||
{ id: 'def-456', name: 'Bob', color: '#8b5cf6' } as Item,
|
||||
]
|
||||
|
||||
describe('<RichTextEditor />', () => {
|
||||
let mockUseTags: ReturnType<typeof vi.fn>
|
||||
let mockUseAddTag: ReturnType<typeof vi.fn>
|
||||
let mockUseItems: ReturnType<typeof vi.fn>
|
||||
let mockUseGetItemColor: ReturnType<typeof vi.fn>
|
||||
let mockAddTag: ReturnType<typeof vi.fn>
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks()
|
||||
mockAddTag = vi.fn()
|
||||
|
||||
const { useTags, useAddTag } = await import('#components/Map/hooks/useTags')
|
||||
const { useItems } = await import('#components/Map/hooks/useItems')
|
||||
const { useGetItemColor } = await import('#components/Map/hooks/useItemColor')
|
||||
|
||||
mockUseTags = vi.mocked(useTags)
|
||||
mockUseAddTag = vi.mocked(useAddTag)
|
||||
mockUseItems = vi.mocked(useItems)
|
||||
mockUseGetItemColor = vi.mocked(useGetItemColor)
|
||||
|
||||
mockUseTags.mockReturnValue(mockTags)
|
||||
mockUseAddTag.mockReturnValue(mockAddTag)
|
||||
mockUseItems.mockReturnValue(mockItems)
|
||||
mockUseGetItemColor.mockReturnValue((item: Item | undefined) => item?.color ?? '#3b82f6')
|
||||
})
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('renders with default value', async () => {
|
||||
render(
|
||||
<Wrapper>
|
||||
<RichTextEditor defaultValue='Hello **world**' />
|
||||
</Wrapper>,
|
||||
)
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('world')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('renders label when labelTitle is provided', async () => {
|
||||
render(
|
||||
<Wrapper>
|
||||
<RichTextEditor defaultValue='Test' labelTitle='Description' />
|
||||
</Wrapper>,
|
||||
)
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Description/)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('renders without label when labelTitle is not provided', async () => {
|
||||
render(
|
||||
<Wrapper>
|
||||
<RichTextEditor defaultValue='Test' />
|
||||
</Wrapper>,
|
||||
)
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Test')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('renders menu by default', async () => {
|
||||
render(
|
||||
<Wrapper>
|
||||
<RichTextEditor defaultValue='Test' />
|
||||
</Wrapper>,
|
||||
)
|
||||
await waitFor(() => {
|
||||
expect(document.querySelector('.editor-wrapper')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('hides menu when showMenu=false', async () => {
|
||||
render(
|
||||
<Wrapper>
|
||||
<RichTextEditor defaultValue='Test' showMenu={false} />
|
||||
</Wrapper>,
|
||||
)
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Test')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Content Editing', () => {
|
||||
it('calls updateFormValue when content changes', async () => {
|
||||
const updateFormValue = vi.fn()
|
||||
render(
|
||||
<Wrapper>
|
||||
<RichTextEditor defaultValue='Initial' updateFormValue={updateFormValue} />
|
||||
</Wrapper>,
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Initial')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
const editor = document.querySelector('.ProseMirror')
|
||||
expect(editor).toBeInTheDocument()
|
||||
|
||||
if (editor) {
|
||||
fireEvent.input(editor, { target: { textContent: 'Updated content' } })
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('Hashtag Rendering', () => {
|
||||
it('renders hashtag with correct styling', async () => {
|
||||
render(
|
||||
<Wrapper>
|
||||
<RichTextEditor defaultValue='Check out #nature' />
|
||||
</Wrapper>,
|
||||
)
|
||||
await waitFor(() => {
|
||||
const hashtag = screen.getByText('#nature')
|
||||
expect(hashtag).toHaveClass('hashtag')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Item Mention Rendering', () => {
|
||||
it('renders item mention with correct styling', async () => {
|
||||
render(
|
||||
<Wrapper>
|
||||
<RichTextEditor defaultValue='Thanks [@Alice](/item/abc-123)' />
|
||||
</Wrapper>,
|
||||
)
|
||||
await waitFor(() => {
|
||||
const mention = screen.getByText('@Alice')
|
||||
expect(mention).toHaveClass('item-mention')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Placeholder', () => {
|
||||
it('renders editor wrapper when empty', async () => {
|
||||
render(
|
||||
<Wrapper>
|
||||
<RichTextEditor defaultValue='' placeholder='Enter description...' />
|
||||
</Wrapper>,
|
||||
)
|
||||
await waitFor(() => {
|
||||
const editor = document.querySelector('.ProseMirror')
|
||||
expect(editor).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -0,0 +1,193 @@
|
||||
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
|
||||
import { MemoryRouter } from 'react-router-dom'
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
|
||||
import { TextView } from './TextView'
|
||||
|
||||
import type { Item } from '#types/Item'
|
||||
import type { Tag } from '#types/Tag'
|
||||
|
||||
const mockNavigate = vi.fn()
|
||||
|
||||
vi.mock('react-router-dom', async () => {
|
||||
const actual = await vi.importActual('react-router-dom')
|
||||
return {
|
||||
...actual,
|
||||
useNavigate: () => mockNavigate,
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('#components/Map/hooks/useTags')
|
||||
vi.mock('#components/Map/hooks/useItems')
|
||||
vi.mock('#components/Map/hooks/useItemColor')
|
||||
vi.mock('#components/Map/hooks/useFilter')
|
||||
|
||||
const mockTags: Tag[] = [
|
||||
{ id: '1', name: 'nature', color: '#22c55e' },
|
||||
{ id: '2', name: 'tech', color: '#3b82f6' },
|
||||
]
|
||||
|
||||
const mockItems: Item[] = [
|
||||
{ id: 'abc-123', name: 'Alice', color: '#ef4444' } as Item,
|
||||
{ id: 'def-456', name: 'Bob', color: '#8b5cf6' } as Item,
|
||||
]
|
||||
|
||||
const Wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<MemoryRouter>{children}</MemoryRouter>
|
||||
)
|
||||
|
||||
describe('<TextView />', () => {
|
||||
let mockUseTags: ReturnType<typeof vi.fn>
|
||||
let mockUseItems: ReturnType<typeof vi.fn>
|
||||
let mockUseGetItemColor: ReturnType<typeof vi.fn>
|
||||
let mockUseAddFilterTag: ReturnType<typeof vi.fn>
|
||||
let mockAddFilterTag: ReturnType<typeof vi.fn>
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks()
|
||||
mockNavigate.mockClear()
|
||||
mockAddFilterTag = vi.fn()
|
||||
|
||||
const { useTags } = await import('#components/Map/hooks/useTags')
|
||||
const { useItems } = await import('#components/Map/hooks/useItems')
|
||||
const { useGetItemColor } = await import('#components/Map/hooks/useItemColor')
|
||||
const { useAddFilterTag } = await import('#components/Map/hooks/useFilter')
|
||||
|
||||
mockUseTags = vi.mocked(useTags)
|
||||
mockUseItems = vi.mocked(useItems)
|
||||
mockUseGetItemColor = vi.mocked(useGetItemColor)
|
||||
mockUseAddFilterTag = vi.mocked(useAddFilterTag)
|
||||
|
||||
mockUseTags.mockReturnValue(mockTags)
|
||||
mockUseItems.mockReturnValue(mockItems)
|
||||
mockUseGetItemColor.mockReturnValue((item: Item | undefined) => item?.color ?? '#3b82f6')
|
||||
mockUseAddFilterTag.mockReturnValue(mockAddFilterTag)
|
||||
})
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('renders markdown content via TipTap', async () => {
|
||||
render(
|
||||
<Wrapper>
|
||||
<TextView text='Hello **world**' />
|
||||
</Wrapper>,
|
||||
)
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('world')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('returns null for empty text', () => {
|
||||
const { container } = render(
|
||||
<Wrapper>
|
||||
<TextView text='' />
|
||||
</Wrapper>,
|
||||
)
|
||||
expect(container.firstChild).toBeNull()
|
||||
})
|
||||
|
||||
it('returns null for null text', () => {
|
||||
const { container } = render(
|
||||
<Wrapper>
|
||||
<TextView text={null} />
|
||||
</Wrapper>,
|
||||
)
|
||||
expect(container.firstChild).toBeNull()
|
||||
})
|
||||
|
||||
it('shows login message when text is undefined', async () => {
|
||||
render(
|
||||
<Wrapper>
|
||||
<TextView text={undefined} />
|
||||
</Wrapper>,
|
||||
)
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Login')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('uses item.text when item prop is provided', async () => {
|
||||
const item = { id: '1', name: 'Test', text: 'Item content here' } as Item
|
||||
render(
|
||||
<Wrapper>
|
||||
<TextView item={item} />
|
||||
</Wrapper>,
|
||||
)
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Item content here/)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Truncation', () => {
|
||||
it('truncates long text when truncate=true', async () => {
|
||||
const longText = 'A'.repeat(150)
|
||||
render(
|
||||
<Wrapper>
|
||||
<TextView text={longText} truncate={true} />
|
||||
</Wrapper>,
|
||||
)
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/\.\.\.$/)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Hashtag Rendering', () => {
|
||||
it('renders hashtag with correct styling', async () => {
|
||||
render(
|
||||
<Wrapper>
|
||||
<TextView text='Check out #nature' />
|
||||
</Wrapper>,
|
||||
)
|
||||
await waitFor(() => {
|
||||
const hashtag = screen.getByText('#nature')
|
||||
expect(hashtag).toHaveClass('hashtag')
|
||||
})
|
||||
})
|
||||
|
||||
it('calls addFilterTag when hashtag is clicked', async () => {
|
||||
render(
|
||||
<Wrapper>
|
||||
<TextView text='Click #nature here' />
|
||||
</Wrapper>,
|
||||
)
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('#nature')).toBeInTheDocument()
|
||||
})
|
||||
const hashtag = screen.getByText('#nature')
|
||||
fireEvent.click(hashtag)
|
||||
expect(mockAddFilterTag).toHaveBeenCalledWith(mockTags[0])
|
||||
})
|
||||
})
|
||||
|
||||
describe('Item Mention Rendering', () => {
|
||||
it('renders item mention with correct styling', async () => {
|
||||
render(
|
||||
<Wrapper>
|
||||
<TextView text='Thanks [@Alice](/item/abc-123)' />
|
||||
</Wrapper>,
|
||||
)
|
||||
await waitFor(() => {
|
||||
const mention = screen.getByText('@Alice')
|
||||
expect(mention).toHaveClass('item-mention')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Link Navigation', () => {
|
||||
it('navigates internally for relative links', async () => {
|
||||
render(
|
||||
<Wrapper>
|
||||
<TextView text='Go to [profile](/profile/123)' />
|
||||
</Wrapper>,
|
||||
)
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('profile')).toBeInTheDocument()
|
||||
})
|
||||
const link = screen.getByText('profile')
|
||||
fireEvent.click(link)
|
||||
expect(mockNavigate).toHaveBeenCalledWith('/profile/123')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -0,0 +1,241 @@
|
||||
import { render, screen, fireEvent } from '@testing-library/react'
|
||||
import { MemoryRouter } from 'react-router-dom'
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
|
||||
import { TextViewStatic } from './TextViewStatic'
|
||||
|
||||
import type { Item } from '#types/Item'
|
||||
import type { Tag } from '#types/Tag'
|
||||
|
||||
const mockNavigate = vi.fn()
|
||||
|
||||
vi.mock('react-router-dom', async () => {
|
||||
const actual = await vi.importActual('react-router-dom')
|
||||
return {
|
||||
...actual,
|
||||
useNavigate: () => mockNavigate,
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('#components/Map/hooks/useTags')
|
||||
vi.mock('#components/Map/hooks/useItems')
|
||||
vi.mock('#components/Map/hooks/useItemColor')
|
||||
vi.mock('#components/Map/hooks/useFilter')
|
||||
|
||||
const mockTags: Tag[] = [
|
||||
{ id: '1', name: 'nature', color: '#22c55e' },
|
||||
{ id: '2', name: 'tech', color: '#3b82f6' },
|
||||
]
|
||||
|
||||
const mockItems: Item[] = [
|
||||
{ id: 'abc-123', name: 'Alice', color: '#ef4444' } as Item,
|
||||
{ id: 'def-456', name: 'Bob', color: '#8b5cf6' } as Item,
|
||||
]
|
||||
|
||||
const Wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<MemoryRouter>{children}</MemoryRouter>
|
||||
)
|
||||
|
||||
describe('<TextViewStatic />', () => {
|
||||
let mockUseTags: ReturnType<typeof vi.fn>
|
||||
let mockUseItems: ReturnType<typeof vi.fn>
|
||||
let mockUseGetItemColor: ReturnType<typeof vi.fn>
|
||||
let mockUseAddFilterTag: ReturnType<typeof vi.fn>
|
||||
let mockAddFilterTag: ReturnType<typeof vi.fn>
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks()
|
||||
mockNavigate.mockClear()
|
||||
mockAddFilterTag = vi.fn()
|
||||
|
||||
const { useTags } = await import('#components/Map/hooks/useTags')
|
||||
const { useItems } = await import('#components/Map/hooks/useItems')
|
||||
const { useGetItemColor } = await import('#components/Map/hooks/useItemColor')
|
||||
const { useAddFilterTag } = await import('#components/Map/hooks/useFilter')
|
||||
|
||||
mockUseTags = vi.mocked(useTags)
|
||||
mockUseItems = vi.mocked(useItems)
|
||||
mockUseGetItemColor = vi.mocked(useGetItemColor)
|
||||
mockUseAddFilterTag = vi.mocked(useAddFilterTag)
|
||||
|
||||
mockUseTags.mockReturnValue(mockTags)
|
||||
mockUseItems.mockReturnValue(mockItems)
|
||||
mockUseGetItemColor.mockReturnValue((item: Item | undefined) => item?.color ?? '#3b82f6')
|
||||
mockUseAddFilterTag.mockReturnValue(mockAddFilterTag)
|
||||
})
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('renders markdown content as HTML', () => {
|
||||
render(
|
||||
<Wrapper>
|
||||
<TextViewStatic text='Hello **world**' />
|
||||
</Wrapper>,
|
||||
)
|
||||
expect(screen.getByText('world')).toBeInTheDocument()
|
||||
expect(screen.getByText('world').tagName).toBe('STRONG')
|
||||
})
|
||||
|
||||
it('returns null for empty text', () => {
|
||||
const { container } = render(
|
||||
<Wrapper>
|
||||
<TextViewStatic text='' />
|
||||
</Wrapper>,
|
||||
)
|
||||
expect(container.firstChild).toBeNull()
|
||||
})
|
||||
|
||||
it('returns null for null text', () => {
|
||||
const { container } = render(
|
||||
<Wrapper>
|
||||
<TextViewStatic text={null} />
|
||||
</Wrapper>,
|
||||
)
|
||||
expect(container.firstChild).toBeNull()
|
||||
})
|
||||
|
||||
it('shows login message when text is undefined', () => {
|
||||
render(
|
||||
<Wrapper>
|
||||
<TextViewStatic text={undefined} />
|
||||
</Wrapper>,
|
||||
)
|
||||
expect(screen.getByText('Login')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('uses item.text when item prop is provided', () => {
|
||||
const item = { id: '1', name: 'Test', text: 'Item content here' } as Item
|
||||
render(
|
||||
<Wrapper>
|
||||
<TextViewStatic item={item} />
|
||||
</Wrapper>,
|
||||
)
|
||||
expect(screen.getByText(/Item content here/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('uses rawText prop directly without processing', () => {
|
||||
render(
|
||||
<Wrapper>
|
||||
<TextViewStatic rawText='Raw **text** content' />
|
||||
</Wrapper>,
|
||||
)
|
||||
expect(screen.getByText('text')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Truncation', () => {
|
||||
it('truncates long text when truncate=true', () => {
|
||||
const longText = 'A'.repeat(150)
|
||||
render(
|
||||
<Wrapper>
|
||||
<TextViewStatic text={longText} truncate={true} />
|
||||
</Wrapper>,
|
||||
)
|
||||
expect(screen.getByText(/\.\.\.$/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('does not truncate when truncate=false', () => {
|
||||
const longText = 'A'.repeat(150)
|
||||
render(
|
||||
<Wrapper>
|
||||
<TextViewStatic text={longText} truncate={false} />
|
||||
</Wrapper>,
|
||||
)
|
||||
expect(screen.queryByText(/\.\.\.$/)).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Hashtag Rendering', () => {
|
||||
it('renders hashtag with correct color', () => {
|
||||
render(
|
||||
<Wrapper>
|
||||
<TextViewStatic text='Check out #nature' />
|
||||
</Wrapper>,
|
||||
)
|
||||
const hashtag = screen.getByText('#nature')
|
||||
expect(hashtag).toHaveStyle({ color: '#22c55e' })
|
||||
})
|
||||
|
||||
it('renders unknown hashtag with inherit color', () => {
|
||||
render(
|
||||
<Wrapper>
|
||||
<TextViewStatic text='Unknown #sometag here' />
|
||||
</Wrapper>,
|
||||
)
|
||||
const hashtag = screen.getByText('#sometag')
|
||||
expect(hashtag).toHaveStyle({ color: 'inherit' })
|
||||
})
|
||||
|
||||
it('calls addFilterTag when hashtag is clicked', () => {
|
||||
render(
|
||||
<Wrapper>
|
||||
<TextViewStatic text='Click #nature here' />
|
||||
</Wrapper>,
|
||||
)
|
||||
const hashtag = screen.getByText('#nature')
|
||||
fireEvent.click(hashtag)
|
||||
expect(mockAddFilterTag).toHaveBeenCalledWith(mockTags[0])
|
||||
})
|
||||
})
|
||||
|
||||
describe('Item Mention Rendering', () => {
|
||||
it('renders item mention as link with correct color', () => {
|
||||
render(
|
||||
<Wrapper>
|
||||
<TextViewStatic text='Thanks [@Alice](/item/abc-123)' />
|
||||
</Wrapper>,
|
||||
)
|
||||
const mention = screen.getByText('@Alice')
|
||||
expect(mention.tagName).toBe('A')
|
||||
expect(mention).toHaveAttribute('href', '/item/abc-123')
|
||||
expect(mention).toHaveStyle({ color: '#ef4444' })
|
||||
})
|
||||
|
||||
it('renders unknown item mention with fallback color', () => {
|
||||
render(
|
||||
<Wrapper>
|
||||
<TextViewStatic text='See [@Unknown](/item/xyz-999)' />
|
||||
</Wrapper>,
|
||||
)
|
||||
const mention = screen.getByText('@Unknown')
|
||||
expect(mention).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Link Navigation', () => {
|
||||
it('navigates internally for relative links', () => {
|
||||
render(
|
||||
<Wrapper>
|
||||
<TextViewStatic text='Go to [profile](/profile/123)' />
|
||||
</Wrapper>,
|
||||
)
|
||||
const link = screen.getByText('profile')
|
||||
fireEvent.click(link)
|
||||
expect(mockNavigate).toHaveBeenCalledWith('/profile/123')
|
||||
})
|
||||
|
||||
it('does not navigate for external links', () => {
|
||||
render(
|
||||
<Wrapper>
|
||||
<TextViewStatic text='Visit [Example](https://example.com)' />
|
||||
</Wrapper>,
|
||||
)
|
||||
const link = screen.getByText('Example')
|
||||
expect(link).toHaveAttribute('target', '_blank')
|
||||
expect(link).toHaveAttribute('rel', 'noopener noreferrer')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Video Embed', () => {
|
||||
it('renders YouTube video as iframe', () => {
|
||||
render(
|
||||
<Wrapper>
|
||||
<TextViewStatic text='Watch <https://www.youtube.com/watch?v=dQw4w9WgXcQ>' />
|
||||
</Wrapper>,
|
||||
)
|
||||
const iframe = document.querySelector('iframe')
|
||||
expect(iframe).toBeInTheDocument()
|
||||
expect(iframe).toHaveAttribute('src', 'https://www.youtube-nocookie.com/embed/dQw4w9WgXcQ')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user