mirror of https://github.com/langgenius/dify.git
test: add unit tests for base components-part-1 (#32154)
This commit is contained in:
parent
f3f56f03e3
commit
84d090db33
|
|
@ -0,0 +1,59 @@
|
|||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import ContentDialog from './index'
|
||||
|
||||
describe('ContentDialog', () => {
|
||||
it('renders children when show is true', async () => {
|
||||
render(
|
||||
<ContentDialog show={true}>
|
||||
<div>Dialog body</div>
|
||||
</ContentDialog>,
|
||||
)
|
||||
|
||||
await screen.findByText('Dialog body')
|
||||
expect(screen.getByText('Dialog body')).toBeInTheDocument()
|
||||
|
||||
const backdrop = document.querySelector('.bg-app-detail-overlay-bg')
|
||||
expect(backdrop).toBeTruthy()
|
||||
})
|
||||
|
||||
it('does not render children when show is false', () => {
|
||||
render(
|
||||
<ContentDialog show={false}>
|
||||
<div>Hidden content</div>
|
||||
</ContentDialog>,
|
||||
)
|
||||
|
||||
expect(screen.queryByText('Hidden content')).toBeNull()
|
||||
expect(document.querySelector('.bg-app-detail-overlay-bg')).toBeNull()
|
||||
})
|
||||
|
||||
it('calls onClose when backdrop is clicked', async () => {
|
||||
const onClose = vi.fn()
|
||||
render(
|
||||
<ContentDialog show={true} onClose={onClose}>
|
||||
<div>Body</div>
|
||||
</ContentDialog>,
|
||||
)
|
||||
|
||||
const user = userEvent.setup()
|
||||
const backdrop = document.querySelector('.bg-app-detail-overlay-bg') as HTMLElement | null
|
||||
expect(backdrop).toBeTruthy()
|
||||
|
||||
await user.click(backdrop!)
|
||||
expect(onClose).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('applies provided className to the content panel', () => {
|
||||
render(
|
||||
<ContentDialog show={true} className="my-panel-class">
|
||||
<div>Panel content</div>
|
||||
</ContentDialog>,
|
||||
)
|
||||
|
||||
const contentPanel = document.querySelector('.bg-app-detail-bg') as HTMLElement | null
|
||||
expect(contentPanel).toBeTruthy()
|
||||
expect(contentPanel?.className).toContain('my-panel-class')
|
||||
expect(screen.getByText('Panel content')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
import { act, render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import CustomDialog from './index'
|
||||
|
||||
describe('CustomDialog Component', () => {
|
||||
const setup = () => userEvent.setup()
|
||||
|
||||
it('should render children and title when show is true', async () => {
|
||||
render(
|
||||
<CustomDialog show={true} title="Modal Title">
|
||||
<div data-testid="dialog-content">Main Content</div>
|
||||
</CustomDialog>,
|
||||
)
|
||||
|
||||
const title = await screen.findByText('Modal Title')
|
||||
const content = screen.getByTestId('dialog-content')
|
||||
|
||||
expect(title).toBeInTheDocument()
|
||||
expect(content).toBeInTheDocument()
|
||||
expect(screen.getByRole('dialog')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render anything when show is false', async () => {
|
||||
render(
|
||||
<CustomDialog show={false} title="Hidden Title">
|
||||
<div>Content</div>
|
||||
</CustomDialog>,
|
||||
)
|
||||
|
||||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('Hidden Title')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should apply the correct semantic tag to title using titleAs', async () => {
|
||||
render(
|
||||
<CustomDialog show={true} title="Semantic Title" titleAs="h1">
|
||||
Content
|
||||
</CustomDialog>,
|
||||
)
|
||||
|
||||
const title = await screen.findByRole('heading', { level: 1 })
|
||||
expect(title).toHaveTextContent('Semantic Title')
|
||||
})
|
||||
|
||||
it('should render the footer only when the prop is provided', async () => {
|
||||
const { rerender } = render(
|
||||
<CustomDialog show={true}>Content</CustomDialog>,
|
||||
)
|
||||
|
||||
await screen.findByRole('dialog')
|
||||
expect(screen.queryByText('Footer Content')).not.toBeInTheDocument()
|
||||
|
||||
rerender(
|
||||
<CustomDialog show={true} footer={<div data-testid="footer-node">Footer Content</div>}>
|
||||
Content
|
||||
</CustomDialog>,
|
||||
)
|
||||
|
||||
expect(await screen.findByTestId('footer-node')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should call onClose when Escape key is pressed', async () => {
|
||||
const user = setup()
|
||||
const onCloseMock = vi.fn()
|
||||
|
||||
render(
|
||||
<CustomDialog show={true} onClose={onCloseMock}>
|
||||
Content
|
||||
</CustomDialog>,
|
||||
)
|
||||
|
||||
await screen.findByRole('dialog')
|
||||
|
||||
await act(async () => {
|
||||
await user.keyboard('{Escape}')
|
||||
})
|
||||
|
||||
expect(onCloseMock).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should call onClose when the backdrop is clicked', async () => {
|
||||
const user = setup()
|
||||
const onCloseMock = vi.fn()
|
||||
|
||||
render(
|
||||
<CustomDialog show={true} onClose={onCloseMock}>
|
||||
Content
|
||||
</CustomDialog>,
|
||||
)
|
||||
|
||||
await screen.findByRole('dialog')
|
||||
|
||||
const backdrop = document.querySelector('.bg-background-overlay-backdrop')
|
||||
expect(backdrop).toBeInTheDocument()
|
||||
|
||||
await act(async () => {
|
||||
await user.click(backdrop!)
|
||||
})
|
||||
|
||||
expect(onCloseMock).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should apply custom class names to internal elements', async () => {
|
||||
render(
|
||||
<CustomDialog
|
||||
show={true}
|
||||
title="Title"
|
||||
className="custom-panel-container"
|
||||
titleClassName="custom-title-style"
|
||||
bodyClassName="custom-body-style"
|
||||
footer="Footer"
|
||||
footerClassName="custom-footer-style"
|
||||
>
|
||||
<div data-testid="content">Content</div>
|
||||
</CustomDialog>,
|
||||
)
|
||||
|
||||
await screen.findByRole('dialog')
|
||||
|
||||
expect(document.querySelector('.custom-panel-container')).toBeInTheDocument()
|
||||
expect(document.querySelector('.custom-title-style')).toBeInTheDocument()
|
||||
expect(document.querySelector('.custom-body-style')).toBeInTheDocument()
|
||||
expect(document.querySelector('.custom-footer-style')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should maintain accessibility attributes (aria-modal)', async () => {
|
||||
render(
|
||||
<CustomDialog show={true} title="Accessibility Test">
|
||||
<button>Focusable Item</button>
|
||||
</CustomDialog>,
|
||||
)
|
||||
|
||||
const dialog = await screen.findByRole('dialog')
|
||||
// Headless UI should automatically set aria-modal="true"
|
||||
expect(dialog).toHaveAttribute('aria-modal', 'true')
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,214 @@
|
|||
import { act, fireEvent, render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import FullScreenModal from './index'
|
||||
|
||||
describe('FullScreenModal Component', () => {
|
||||
it('should not render anything when open is false', () => {
|
||||
render(
|
||||
<FullScreenModal open={false}>
|
||||
<div data-testid="modal-content">Content</div>
|
||||
</FullScreenModal>,
|
||||
)
|
||||
expect(screen.queryByTestId('modal-content')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render content when open is true', async () => {
|
||||
render(
|
||||
<FullScreenModal open={true}>
|
||||
<div data-testid="modal-content">Content</div>
|
||||
</FullScreenModal>,
|
||||
)
|
||||
expect(await screen.findByTestId('modal-content')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not crash when provided with title and description props', async () => {
|
||||
await act(async () => {
|
||||
render(
|
||||
<FullScreenModal
|
||||
open={true}
|
||||
title="My Title"
|
||||
description="My Description"
|
||||
>
|
||||
Content
|
||||
</FullScreenModal>,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Props Handling', () => {
|
||||
it('should apply wrapperClassName to the dialog root', async () => {
|
||||
render(
|
||||
<FullScreenModal
|
||||
open={true}
|
||||
wrapperClassName="custom-wrapper-class"
|
||||
>
|
||||
Content
|
||||
</FullScreenModal>,
|
||||
)
|
||||
|
||||
await screen.findByRole('dialog')
|
||||
const element = document.querySelector('.custom-wrapper-class')
|
||||
expect(element).toBeInTheDocument()
|
||||
expect(element).toHaveClass('modal-dialog')
|
||||
})
|
||||
|
||||
it('should apply className to the inner panel', async () => {
|
||||
await act(async () => {
|
||||
render(
|
||||
<FullScreenModal
|
||||
open={true}
|
||||
className="custom-panel-class"
|
||||
>
|
||||
Content
|
||||
</FullScreenModal>,
|
||||
)
|
||||
})
|
||||
const panel = document.querySelector('.custom-panel-class')
|
||||
expect(panel).toBeInTheDocument()
|
||||
expect(panel).toHaveClass('h-full')
|
||||
})
|
||||
|
||||
it('should handle overflowVisible prop', async () => {
|
||||
const { rerender } = await act(async () => {
|
||||
return render(
|
||||
<FullScreenModal
|
||||
open={true}
|
||||
overflowVisible={true}
|
||||
className="target-panel"
|
||||
>
|
||||
Content
|
||||
</FullScreenModal>,
|
||||
)
|
||||
})
|
||||
let panel = document.querySelector('.target-panel')
|
||||
expect(panel).toHaveClass('overflow-visible')
|
||||
expect(panel).not.toHaveClass('overflow-hidden')
|
||||
|
||||
await act(async () => {
|
||||
rerender(
|
||||
<FullScreenModal
|
||||
open={true}
|
||||
overflowVisible={false}
|
||||
className="target-panel"
|
||||
>
|
||||
Content
|
||||
</FullScreenModal>,
|
||||
)
|
||||
})
|
||||
panel = document.querySelector('.target-panel')
|
||||
expect(panel).toHaveClass('overflow-hidden')
|
||||
expect(panel).not.toHaveClass('overflow-visible')
|
||||
})
|
||||
|
||||
it('should render close button when closable is true', async () => {
|
||||
await act(async () => {
|
||||
render(
|
||||
<FullScreenModal open={true} closable={true}>
|
||||
Content
|
||||
</FullScreenModal>,
|
||||
)
|
||||
})
|
||||
const closeButton = document.querySelector('.bg-components-button-tertiary-bg')
|
||||
expect(closeButton).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render close button when closable is false', async () => {
|
||||
await act(async () => {
|
||||
render(
|
||||
<FullScreenModal open={true} closable={false}>
|
||||
Content
|
||||
</FullScreenModal>,
|
||||
)
|
||||
})
|
||||
const closeButton = document.querySelector('.bg-components-button-tertiary-bg')
|
||||
expect(closeButton).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Interactions', () => {
|
||||
it('should call onClose when close button is clicked', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onClose = vi.fn()
|
||||
|
||||
render(
|
||||
<FullScreenModal open={true} closable={true} onClose={onClose}>
|
||||
Content
|
||||
</FullScreenModal>,
|
||||
)
|
||||
|
||||
const closeBtn = document.querySelector('.bg-components-button-tertiary-bg')
|
||||
expect(closeBtn).toBeInTheDocument()
|
||||
|
||||
await user.click(closeBtn!)
|
||||
expect(onClose).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should call onClose when clicking the backdrop', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onClose = vi.fn()
|
||||
|
||||
render(
|
||||
<FullScreenModal open={true} onClose={onClose}>
|
||||
<div data-testid="inner">Content</div>
|
||||
</FullScreenModal>,
|
||||
)
|
||||
|
||||
const dialog = document.querySelector('.modal-dialog')
|
||||
if (dialog) {
|
||||
await user.click(dialog)
|
||||
expect(onClose).toHaveBeenCalled()
|
||||
}
|
||||
else {
|
||||
throw new Error('Dialog root not found')
|
||||
}
|
||||
})
|
||||
|
||||
it('should call onClose when Escape key is pressed', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onClose = vi.fn()
|
||||
|
||||
render(
|
||||
<FullScreenModal open={true} onClose={onClose}>
|
||||
Content
|
||||
</FullScreenModal>,
|
||||
)
|
||||
|
||||
await user.keyboard('{Escape}')
|
||||
expect(onClose).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not call onClose when clicking inside the content', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onClose = vi.fn()
|
||||
|
||||
render(
|
||||
<FullScreenModal open={true} onClose={onClose}>
|
||||
<div className="bg-background-default-subtle">
|
||||
<button>Action</button>
|
||||
</div>
|
||||
</FullScreenModal>,
|
||||
)
|
||||
|
||||
const innerButton = screen.getByRole('button', { name: 'Action' })
|
||||
await user.click(innerButton)
|
||||
expect(onClose).not.toHaveBeenCalled()
|
||||
|
||||
const contentPanel = document.querySelector('.bg-background-default-subtle')
|
||||
await act(async () => {
|
||||
fireEvent.click(contentPanel!)
|
||||
})
|
||||
expect(onClose).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Default Props', () => {
|
||||
it('should not throw if onClose is not provided', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<FullScreenModal open={true} closable={true}>Content</FullScreenModal>)
|
||||
|
||||
const closeButton = document.querySelector('.bg-components-button-tertiary-bg')
|
||||
await user.click(closeButton!)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,205 @@
|
|||
import { act, render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import i18next from 'i18next'
|
||||
import { useParams, usePathname } from 'next/navigation'
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import AudioBtn from './index'
|
||||
|
||||
const mockPlayAudio = vi.fn()
|
||||
const mockPauseAudio = vi.fn()
|
||||
const mockGetAudioPlayer = vi.fn()
|
||||
|
||||
vi.mock('next/navigation', () => ({
|
||||
useParams: vi.fn(),
|
||||
usePathname: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/audio-btn/audio.player.manager', () => ({
|
||||
AudioPlayerManager: {
|
||||
getInstance: vi.fn(() => ({
|
||||
getAudioPlayer: mockGetAudioPlayer,
|
||||
})),
|
||||
},
|
||||
}))
|
||||
|
||||
describe('AudioBtn', () => {
|
||||
const getButton = () => screen.getByRole('button')
|
||||
|
||||
const hoverAndCheckTooltip = async (expectedText: string) => {
|
||||
const button = getButton()
|
||||
await userEvent.hover(button)
|
||||
expect(await screen.findByText(expectedText)).toBeInTheDocument()
|
||||
}
|
||||
|
||||
const getAudioCallback = () => {
|
||||
const lastCall = mockGetAudioPlayer.mock.calls[mockGetAudioPlayer.mock.calls.length - 1]
|
||||
const callback = lastCall?.find((arg: unknown) => typeof arg === 'function') as ((event: string) => void) | undefined
|
||||
if (!callback)
|
||||
throw new Error('Audio callback not found - ensure mockGetAudioPlayer was called with a callback argument')
|
||||
return callback
|
||||
}
|
||||
|
||||
beforeAll(() => {
|
||||
i18next.init({})
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockGetAudioPlayer.mockReturnValue({
|
||||
playAudio: mockPlayAudio,
|
||||
pauseAudio: mockPauseAudio,
|
||||
})
|
||||
; (useParams as ReturnType<typeof vi.fn>).mockReturnValue({})
|
||||
; (usePathname as ReturnType<typeof vi.fn>).mockReturnValue('/')
|
||||
})
|
||||
|
||||
describe('URL Routing', () => {
|
||||
it('should generate public URL when token is present', async () => {
|
||||
; (useParams as ReturnType<typeof vi.fn>).mockReturnValue({ token: 'test-token' })
|
||||
|
||||
render(<AudioBtn value="test" />)
|
||||
await userEvent.click(getButton())
|
||||
|
||||
await waitFor(() => expect(mockGetAudioPlayer).toHaveBeenCalled())
|
||||
expect(mockGetAudioPlayer.mock.calls[0][0]).toBe('/text-to-audio')
|
||||
expect(mockGetAudioPlayer.mock.calls[0][1]).toBe(true)
|
||||
})
|
||||
|
||||
it('should generate app URL when appId is present', async () => {
|
||||
; (useParams as ReturnType<typeof vi.fn>).mockReturnValue({ appId: '123' })
|
||||
; (usePathname as ReturnType<typeof vi.fn>).mockReturnValue('/apps/123/chat')
|
||||
|
||||
render(<AudioBtn value="test" />)
|
||||
await userEvent.click(getButton())
|
||||
|
||||
await waitFor(() => expect(mockGetAudioPlayer).toHaveBeenCalled())
|
||||
expect(mockGetAudioPlayer.mock.calls[0][0]).toBe('/apps/123/text-to-audio')
|
||||
expect(mockGetAudioPlayer.mock.calls[0][1]).toBe(false)
|
||||
})
|
||||
|
||||
it('should generate installed app URL correctly', async () => {
|
||||
; (useParams as ReturnType<typeof vi.fn>).mockReturnValue({ appId: '456' })
|
||||
; (usePathname as ReturnType<typeof vi.fn>).mockReturnValue('/explore/installed/app')
|
||||
|
||||
render(<AudioBtn value="test" />)
|
||||
await userEvent.click(getButton())
|
||||
|
||||
await waitFor(() => expect(mockGetAudioPlayer).toHaveBeenCalled())
|
||||
expect(mockGetAudioPlayer.mock.calls[0][0]).toBe('/installed-apps/456/text-to-audio')
|
||||
})
|
||||
})
|
||||
|
||||
describe('State Management', () => {
|
||||
it('should start in initial state', async () => {
|
||||
render(<AudioBtn value="test" />)
|
||||
|
||||
await hoverAndCheckTooltip('play')
|
||||
expect(getButton()).toHaveClass('action-btn')
|
||||
expect(getButton()).not.toBeDisabled()
|
||||
})
|
||||
|
||||
it('should transition to playing state', async () => {
|
||||
render(<AudioBtn value="test" />)
|
||||
await userEvent.click(getButton())
|
||||
|
||||
act(() => {
|
||||
getAudioCallback()('play')
|
||||
})
|
||||
|
||||
await hoverAndCheckTooltip('playing')
|
||||
expect(getButton()).toHaveClass('action-btn-active')
|
||||
})
|
||||
|
||||
it('should transition to ended state', async () => {
|
||||
render(<AudioBtn value="test" />)
|
||||
await userEvent.click(getButton())
|
||||
|
||||
act(() => {
|
||||
getAudioCallback()('play')
|
||||
})
|
||||
act(() => {
|
||||
getAudioCallback()('ended')
|
||||
})
|
||||
|
||||
await hoverAndCheckTooltip('play')
|
||||
expect(getButton()).not.toHaveClass('action-btn-active')
|
||||
})
|
||||
|
||||
it('should handle paused event', async () => {
|
||||
render(<AudioBtn value="test" />)
|
||||
await userEvent.click(getButton())
|
||||
|
||||
act(() => {
|
||||
getAudioCallback()('play')
|
||||
})
|
||||
act(() => {
|
||||
getAudioCallback()('paused')
|
||||
})
|
||||
|
||||
await hoverAndCheckTooltip('play')
|
||||
})
|
||||
|
||||
it('should handle error event', async () => {
|
||||
render(<AudioBtn value="test" />)
|
||||
await userEvent.click(getButton())
|
||||
|
||||
act(() => {
|
||||
getAudioCallback()('error')
|
||||
})
|
||||
|
||||
await hoverAndCheckTooltip('play')
|
||||
})
|
||||
|
||||
it('should handle loaded event', async () => {
|
||||
render(<AudioBtn value="test" />)
|
||||
await userEvent.click(getButton())
|
||||
|
||||
act(() => {
|
||||
getAudioCallback()('loaded')
|
||||
})
|
||||
|
||||
await hoverAndCheckTooltip('loading')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Play/Pause', () => {
|
||||
it('should call playAudio when clicked', async () => {
|
||||
render(<AudioBtn value="test" />)
|
||||
await userEvent.click(getButton())
|
||||
|
||||
await waitFor(() => expect(mockPlayAudio).toHaveBeenCalled())
|
||||
})
|
||||
|
||||
it('should call pauseAudio when clicked while playing', async () => {
|
||||
render(<AudioBtn value="test" />)
|
||||
await userEvent.click(getButton())
|
||||
|
||||
act(() => {
|
||||
getAudioCallback()('play')
|
||||
})
|
||||
|
||||
await userEvent.click(getButton())
|
||||
await waitFor(() => expect(mockPauseAudio).toHaveBeenCalled())
|
||||
})
|
||||
|
||||
it('should disable button when loading', async () => {
|
||||
render(<AudioBtn value="test" />)
|
||||
await userEvent.click(getButton())
|
||||
|
||||
await waitFor(() => expect(getButton()).toBeDisabled())
|
||||
})
|
||||
})
|
||||
|
||||
describe('Props', () => {
|
||||
it('should pass props to audio player', async () => {
|
||||
render(<AudioBtn value="hello" id="msg-1" voice="en-US" />)
|
||||
await userEvent.click(getButton())
|
||||
|
||||
await waitFor(() => expect(mockGetAudioPlayer).toHaveBeenCalled())
|
||||
const call = mockGetAudioPlayer.mock.calls[0]
|
||||
expect(call[2]).toBe('msg-1')
|
||||
expect(call[3]).toBe('hello')
|
||||
expect(call[4]).toBe('en-US')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import NotionConnector from './index'
|
||||
|
||||
describe('NotionConnector', () => {
|
||||
it('should render the layout and actual sub-components (Icons & Button)', () => {
|
||||
const { container } = render(<NotionConnector onSetting={vi.fn()} />)
|
||||
|
||||
// Verify Title & Tip translations
|
||||
expect(screen.getByText('datasetCreation.stepOne.notionSyncTitle')).toBeInTheDocument()
|
||||
expect(screen.getByText('datasetCreation.stepOne.notionSyncTip')).toBeInTheDocument()
|
||||
|
||||
const notionWrapper = container.querySelector('.h-12.w-12')
|
||||
const dotsWrapper = container.querySelector('.system-md-semibold')
|
||||
|
||||
expect(notionWrapper?.querySelector('svg')).toBeInTheDocument()
|
||||
expect(dotsWrapper?.querySelector('svg')).toBeInTheDocument()
|
||||
|
||||
const button = screen.getByRole('button', {
|
||||
name: /datasetcreation.stepone.connect/i,
|
||||
})
|
||||
|
||||
expect(button).toBeInTheDocument()
|
||||
expect(button).toHaveClass('btn', 'btn-primary')
|
||||
})
|
||||
|
||||
it('should trigger the onSetting callback when the real button is clicked', async () => {
|
||||
const onSetting = vi.fn()
|
||||
const user = userEvent.setup()
|
||||
render(<NotionConnector onSetting={onSetting} />)
|
||||
|
||||
const button = screen.getByRole('button', {
|
||||
name: /datasetcreation.stepone.connect/i,
|
||||
})
|
||||
|
||||
await user.click(button)
|
||||
|
||||
expect(onSetting).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should maintain the correct visual hierarchy classes', () => {
|
||||
const { container } = render(<NotionConnector onSetting={vi.fn()} />)
|
||||
|
||||
// Verify the outer container has the specific workflow-process-bg
|
||||
const mainContainer = container.firstChild
|
||||
expect(mainContainer).toHaveClass('bg-workflow-process-bg', 'rounded-2xl', 'p-6')
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
SkeletonContainer,
|
||||
SkeletonPoint,
|
||||
SkeletonRectangle,
|
||||
SkeletonRow,
|
||||
} from './index'
|
||||
|
||||
describe('Skeleton Components', () => {
|
||||
describe('Individual Components', () => {
|
||||
it('should forward attributes and render children in SkeletonContainer', () => {
|
||||
render(
|
||||
<SkeletonContainer data-testid="container" className="custom-container">
|
||||
<span>Content</span>
|
||||
</SkeletonContainer>,
|
||||
)
|
||||
const element = screen.getByTestId('container')
|
||||
expect(element).toHaveClass('flex', 'flex-col', 'custom-container')
|
||||
expect(screen.getByText('Content')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should forward attributes and render children in SkeletonRow', () => {
|
||||
render(
|
||||
<SkeletonRow data-testid="row" className="custom-row">
|
||||
<span>Row Content</span>
|
||||
</SkeletonRow>,
|
||||
)
|
||||
const element = screen.getByTestId('row')
|
||||
expect(element).toHaveClass('flex', 'items-center', 'custom-row')
|
||||
expect(screen.getByText('Row Content')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should apply base skeleton styles to SkeletonRectangle', () => {
|
||||
render(<SkeletonRectangle data-testid="rect" className="w-10" />)
|
||||
const element = screen.getByTestId('rect')
|
||||
expect(element).toHaveClass('h-2', 'bg-text-quaternary', 'opacity-20', 'w-10')
|
||||
})
|
||||
|
||||
it('should render the separator character correctly in SkeletonPoint', () => {
|
||||
render(<SkeletonPoint data-testid="point" />)
|
||||
const element = screen.getByTestId('point')
|
||||
expect(element).toHaveTextContent('·')
|
||||
expect(element).toHaveClass('text-text-quaternary')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Composition & Layout', () => {
|
||||
it('should render a full skeleton structure accurately', () => {
|
||||
const { container } = render(
|
||||
<SkeletonContainer className="main-wrapper">
|
||||
<SkeletonRow>
|
||||
<SkeletonRectangle className="rect-1" />
|
||||
<SkeletonPoint />
|
||||
<SkeletonRectangle className="rect-2" />
|
||||
</SkeletonRow>
|
||||
</SkeletonContainer>,
|
||||
)
|
||||
|
||||
const wrapper = container.firstChild as HTMLElement
|
||||
expect(wrapper).toHaveClass('main-wrapper')
|
||||
|
||||
expect(container.querySelector('.rect-1')).toBeInTheDocument()
|
||||
expect(container.querySelector('.rect-2')).toBeInTheDocument()
|
||||
|
||||
const row = container.querySelector('.flex.items-center')
|
||||
expect(row).toContainElement(container.querySelector('.rect-1') as HTMLElement)
|
||||
expect(row).toHaveTextContent('·')
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle rest props like event listeners', async () => {
|
||||
const onClick = vi.fn()
|
||||
const user = userEvent.setup()
|
||||
render(<SkeletonRectangle onClick={onClick} data-testid="clickable" />)
|
||||
|
||||
const element = screen.getByTestId('clickable')
|
||||
|
||||
await user.click(element)
|
||||
expect(onClick).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
import { act, render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import Slider from './index'
|
||||
|
||||
describe('Slider Component', () => {
|
||||
it('should render with correct default ARIA limits and current value', () => {
|
||||
render(<Slider value={50} onChange={vi.fn()} />)
|
||||
|
||||
const slider = screen.getByRole('slider')
|
||||
expect(slider).toHaveAttribute('aria-valuemin', '0')
|
||||
expect(slider).toHaveAttribute('aria-valuemax', '100')
|
||||
expect(slider).toHaveAttribute('aria-valuenow', '50')
|
||||
})
|
||||
|
||||
it('should apply custom min, max, and step values', () => {
|
||||
render(<Slider value={10} min={5} max={20} step={5} onChange={vi.fn()} />)
|
||||
|
||||
const slider = screen.getByRole('slider')
|
||||
expect(slider).toHaveAttribute('aria-valuemin', '5')
|
||||
expect(slider).toHaveAttribute('aria-valuemax', '20')
|
||||
expect(slider).toHaveAttribute('aria-valuenow', '10')
|
||||
})
|
||||
|
||||
it('should default to 0 if the value prop is NaN', () => {
|
||||
render(<Slider value={Number.NaN} onChange={vi.fn()} />)
|
||||
|
||||
const slider = screen.getByRole('slider')
|
||||
expect(slider).toHaveAttribute('aria-valuenow', '0')
|
||||
})
|
||||
|
||||
it('should call onChange when arrow keys are pressed', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onChange = vi.fn()
|
||||
|
||||
render(<Slider value={20} onChange={onChange} />)
|
||||
|
||||
const slider = screen.getByRole('slider')
|
||||
|
||||
await act(async () => {
|
||||
slider.focus()
|
||||
await user.keyboard('{ArrowRight}')
|
||||
})
|
||||
|
||||
expect(onChange).toHaveBeenCalledTimes(1)
|
||||
expect(onChange).toHaveBeenCalledWith(21, 0)
|
||||
})
|
||||
|
||||
it('should not trigger onChange when disabled', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onChange = vi.fn()
|
||||
render(<Slider value={20} onChange={onChange} disabled />)
|
||||
|
||||
const slider = screen.getByRole('slider')
|
||||
|
||||
expect(slider).toHaveAttribute('aria-disabled', 'true')
|
||||
|
||||
await act(async () => {
|
||||
slider.focus()
|
||||
await user.keyboard('{ArrowRight}')
|
||||
})
|
||||
|
||||
expect(onChange).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should apply custom class names', () => {
|
||||
render(
|
||||
<Slider value={10} onChange={vi.fn()} className="outer-test" thumbClassName="thumb-test" />,
|
||||
)
|
||||
|
||||
const sliderWrapper = screen.getByRole('slider').closest('.outer-test')
|
||||
expect(sliderWrapper).toBeInTheDocument()
|
||||
|
||||
const thumb = screen.getByRole('slider')
|
||||
expect(thumb).toHaveClass('thumb-test')
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,141 @@
|
|||
import { render, screen, waitFor, within } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import * as React from 'react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import Sort from './index'
|
||||
|
||||
const mockItems = [
|
||||
{ value: 'created_at', name: 'Date Created' },
|
||||
{ value: 'name', name: 'Name' },
|
||||
{ value: 'status', name: 'Status' },
|
||||
]
|
||||
|
||||
describe('Sort component — real portal integration', () => {
|
||||
const setup = (props = {}) => {
|
||||
const onSelect = vi.fn()
|
||||
const user = userEvent.setup()
|
||||
const { container, rerender } = render(
|
||||
<Sort value="created_at" items={mockItems} onSelect={onSelect} order="" {...props} />,
|
||||
)
|
||||
|
||||
// helper: returns a non-null HTMLElement or throws with a clear message
|
||||
const getTriggerWrapper = (): HTMLElement => {
|
||||
const labelNode = screen.getByText('appLog.filter.sortBy')
|
||||
// try to find a reasonable wrapper element; prefer '.block' but fallback to any ancestor div
|
||||
const wrapper = labelNode.closest('.block') ?? labelNode.closest('div')
|
||||
if (!wrapper)
|
||||
throw new Error('Trigger wrapper element not found for "Sort by" label')
|
||||
return wrapper as HTMLElement
|
||||
}
|
||||
|
||||
// helper: returns right-side sort button element
|
||||
const getSortButton = (): HTMLElement => {
|
||||
const btn = container.querySelector('.rounded-r-lg')
|
||||
if (!btn)
|
||||
throw new Error('Sort button (rounded-r-lg) not found in rendered container')
|
||||
return btn as HTMLElement
|
||||
}
|
||||
|
||||
return { user, onSelect, rerender, getTriggerWrapper, getSortButton }
|
||||
}
|
||||
|
||||
it('renders and shows selected item label and sort icon', () => {
|
||||
const { getSortButton } = setup({ order: '' })
|
||||
|
||||
expect(screen.getByText('Date Created')).toBeInTheDocument()
|
||||
|
||||
const sortButton = getSortButton()
|
||||
expect(sortButton).toBeInstanceOf(HTMLElement)
|
||||
expect(sortButton.querySelector('svg')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('opens and closes the tooltip (portal mounts to document.body)', async () => {
|
||||
const { user, getTriggerWrapper } = setup()
|
||||
|
||||
await user.click(getTriggerWrapper())
|
||||
const tooltip = await screen.findByRole('tooltip')
|
||||
expect(tooltip).toBeInTheDocument()
|
||||
expect(document.body.contains(tooltip)).toBe(true)
|
||||
|
||||
// clicking the trigger again should close it
|
||||
await user.click(getTriggerWrapper())
|
||||
await waitFor(() => expect(screen.queryByRole('tooltip')).not.toBeInTheDocument())
|
||||
})
|
||||
|
||||
it('renders options and calls onSelect with descending prefix when order is "-"', async () => {
|
||||
const { user, onSelect, getTriggerWrapper } = setup({ order: '-' })
|
||||
|
||||
await user.click(getTriggerWrapper())
|
||||
const tooltip = await screen.findByRole('tooltip')
|
||||
|
||||
mockItems.forEach((item) => {
|
||||
expect(within(tooltip).getByText(item.name)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
await user.click(within(tooltip).getByText('Name'))
|
||||
expect(onSelect).toHaveBeenCalledWith('-name')
|
||||
await waitFor(() => expect(screen.queryByRole('tooltip')).not.toBeInTheDocument())
|
||||
})
|
||||
|
||||
it('toggles sorting order: ascending -> descending via right-side button', async () => {
|
||||
const { user, onSelect, getSortButton } = setup({ order: '', value: 'created_at' })
|
||||
await user.click(getSortButton())
|
||||
expect(onSelect).toHaveBeenCalledWith('-created_at')
|
||||
})
|
||||
|
||||
it('toggles sorting order: descending -> ascending via right-side button', async () => {
|
||||
const { user, onSelect, getSortButton } = setup({ order: '-', value: 'name' })
|
||||
await user.click(getSortButton())
|
||||
expect(onSelect).toHaveBeenCalledWith('name')
|
||||
})
|
||||
|
||||
it('shows checkmark only for selected item in menu', async () => {
|
||||
const { user, getTriggerWrapper } = setup({ value: 'status' })
|
||||
|
||||
await user.click(getTriggerWrapper())
|
||||
const tooltip = await screen.findByRole('tooltip')
|
||||
|
||||
const statusRow = within(tooltip).getByText('Status').closest('.flex')
|
||||
const nameRow = within(tooltip).getByText('Name').closest('.flex')
|
||||
|
||||
if (!statusRow)
|
||||
throw new Error('Status option row not found in menu')
|
||||
if (!nameRow)
|
||||
throw new Error('Name option row not found in menu')
|
||||
|
||||
expect(statusRow.querySelector('svg')).toBeInTheDocument()
|
||||
expect(nameRow.querySelector('svg')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows empty selection label when value is unknown', () => {
|
||||
setup({ value: 'unknown_value' })
|
||||
const label = screen.getByText('appLog.filter.sortBy')
|
||||
const valueNode = label.nextSibling
|
||||
if (!valueNode)
|
||||
throw new Error('Expected a sibling node for the selection text')
|
||||
expect(String(valueNode.textContent || '').trim()).toBe('')
|
||||
})
|
||||
|
||||
it('handles undefined order prop without asserting a literal "undefined" prefix', async () => {
|
||||
const { user, onSelect, getTriggerWrapper } = setup({ order: undefined })
|
||||
|
||||
await user.click(getTriggerWrapper())
|
||||
const tooltip = await screen.findByRole('tooltip')
|
||||
|
||||
await user.click(within(tooltip).getByText('Name'))
|
||||
|
||||
expect(onSelect).toHaveBeenCalled()
|
||||
expect(onSelect).toHaveBeenCalledWith(expect.stringMatching(/name$/))
|
||||
})
|
||||
|
||||
it('clicking outside the open menu closes the portal', async () => {
|
||||
const { user, getTriggerWrapper } = setup()
|
||||
await user.click(getTriggerWrapper())
|
||||
const tooltip = await screen.findByRole('tooltip')
|
||||
expect(tooltip).toBeInTheDocument()
|
||||
|
||||
// click outside: body click should close the tooltip
|
||||
await user.click(document.body)
|
||||
await waitFor(() => expect(screen.queryByRole('tooltip')).not.toBeInTheDocument())
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import Switch from './index'
|
||||
|
||||
describe('Switch', () => {
|
||||
it('should render in unchecked state by default', () => {
|
||||
render(<Switch />)
|
||||
const switchElement = screen.getByRole('switch')
|
||||
expect(switchElement).toBeInTheDocument()
|
||||
expect(switchElement).toHaveAttribute('aria-checked', 'false')
|
||||
})
|
||||
|
||||
it('should render in checked state when defaultValue is true', () => {
|
||||
render(<Switch defaultValue={true} />)
|
||||
const switchElement = screen.getByRole('switch')
|
||||
expect(switchElement).toHaveAttribute('aria-checked', 'true')
|
||||
})
|
||||
|
||||
it('should toggle state and call onChange when clicked', async () => {
|
||||
const onChange = vi.fn()
|
||||
const user = userEvent.setup()
|
||||
render(<Switch onChange={onChange} />)
|
||||
|
||||
const switchElement = screen.getByRole('switch')
|
||||
|
||||
await user.click(switchElement)
|
||||
expect(switchElement).toHaveAttribute('aria-checked', 'true')
|
||||
expect(onChange).toHaveBeenCalledWith(true)
|
||||
expect(onChange).toHaveBeenCalledTimes(1)
|
||||
|
||||
await user.click(switchElement)
|
||||
expect(switchElement).toHaveAttribute('aria-checked', 'false')
|
||||
expect(onChange).toHaveBeenCalledWith(false)
|
||||
expect(onChange).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('should not call onChange when disabled', async () => {
|
||||
const onChange = vi.fn()
|
||||
const user = userEvent.setup()
|
||||
render(<Switch disabled onChange={onChange} />)
|
||||
|
||||
const switchElement = screen.getByRole('switch')
|
||||
expect(switchElement).toHaveClass('!cursor-not-allowed', '!opacity-50')
|
||||
|
||||
await user.click(switchElement)
|
||||
expect(onChange).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should apply correct size classes', () => {
|
||||
const { rerender } = render(<Switch size="xs" />)
|
||||
// We only need to find the element once
|
||||
const switchElement = screen.getByRole('switch')
|
||||
expect(switchElement).toHaveClass('h-2.5', 'w-3.5', 'rounded-sm')
|
||||
|
||||
rerender(<Switch size="sm" />)
|
||||
expect(switchElement).toHaveClass('h-3', 'w-5')
|
||||
|
||||
rerender(<Switch size="md" />)
|
||||
expect(switchElement).toHaveClass('h-4', 'w-7')
|
||||
|
||||
rerender(<Switch size="l" />)
|
||||
expect(switchElement).toHaveClass('h-5', 'w-9')
|
||||
|
||||
rerender(<Switch size="lg" />)
|
||||
expect(switchElement).toHaveClass('h-6', 'w-11')
|
||||
})
|
||||
|
||||
it('should apply custom className', () => {
|
||||
render(<Switch className="custom-test-class" />)
|
||||
expect(screen.getByRole('switch')).toHaveClass('custom-test-class')
|
||||
})
|
||||
|
||||
it('should apply correct background colors based on state', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<Switch />)
|
||||
const switchElement = screen.getByRole('switch')
|
||||
|
||||
expect(switchElement).toHaveClass('bg-components-toggle-bg-unchecked')
|
||||
|
||||
await user.click(switchElement)
|
||||
expect(switchElement).toHaveClass('bg-components-toggle-bg')
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
import { render, screen } from '@testing-library/react'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import Tag from './index'
|
||||
import '@testing-library/jest-dom/vitest'
|
||||
|
||||
describe('Tag Component', () => {
|
||||
describe('Rendering', () => {
|
||||
it('should render with text children', () => {
|
||||
const { container } = render(<Tag>Hello World</Tag>)
|
||||
expect(container.firstChild).toHaveTextContent('Hello World')
|
||||
})
|
||||
|
||||
it('should render with ReactNode children', () => {
|
||||
render(<Tag><span data-testid="child">Node</span></Tag>)
|
||||
expect(screen.getByTestId('child')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should always apply base layout classes', () => {
|
||||
const { container } = render(<Tag>Test</Tag>)
|
||||
expect(container.firstChild).toHaveClass(
|
||||
'inline-flex',
|
||||
'shrink-0',
|
||||
'items-center',
|
||||
'rounded-md',
|
||||
'px-2.5',
|
||||
'py-px',
|
||||
'text-xs',
|
||||
'leading-5',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Color Variants', () => {
|
||||
it.each([
|
||||
{ color: 'green', text: 'text-green-800', bg: 'bg-green-100' },
|
||||
{ color: 'yellow', text: 'text-yellow-800', bg: 'bg-yellow-100' },
|
||||
{ color: 'red', text: 'text-red-800', bg: 'bg-red-100' },
|
||||
{ color: 'gray', text: 'text-gray-800', bg: 'bg-gray-100' },
|
||||
])('should apply $color color classes', ({ color, text, bg }) => {
|
||||
type colorType = 'green' | 'yellow' | 'red' | 'gray' | undefined
|
||||
const { container } = render(<Tag color={color as colorType}>Test</Tag>)
|
||||
expect(container.firstChild).toHaveClass(text, bg)
|
||||
})
|
||||
|
||||
it('should default to green when no color specified', () => {
|
||||
const { container } = render(<Tag>Test</Tag>)
|
||||
expect(container.firstChild).toHaveClass('text-green-800', 'bg-green-100')
|
||||
})
|
||||
|
||||
it('should not apply color classes for invalid color', () => {
|
||||
type colorType = 'green' | 'yellow' | 'red' | 'gray' | undefined
|
||||
const { container } = render(<Tag color={'invalid' as colorType}>Test</Tag>)
|
||||
const className = (container.firstChild as HTMLElement)?.className || ''
|
||||
|
||||
expect(className).not.toMatch(/text-(green|yellow|red|gray)-800/)
|
||||
expect(className).not.toMatch(/bg-(green|yellow|red|gray)-100/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Boolean Props', () => {
|
||||
it('should apply border when bordered is true', () => {
|
||||
const { container } = render(<Tag bordered>Test</Tag>)
|
||||
expect(container.firstChild).toHaveClass('border-[1px]')
|
||||
})
|
||||
|
||||
it('should not apply border by default', () => {
|
||||
const { container } = render(<Tag>Test</Tag>)
|
||||
expect(container.firstChild).not.toHaveClass('border-[1px]')
|
||||
})
|
||||
|
||||
it('should hide background when hideBg is true', () => {
|
||||
const { container } = render(<Tag hideBg>Test</Tag>)
|
||||
expect(container.firstChild).toHaveClass('bg-transparent')
|
||||
})
|
||||
|
||||
it('should apply both bordered and hideBg together', () => {
|
||||
const { container } = render(<Tag bordered hideBg>Test</Tag>)
|
||||
expect(container.firstChild).toHaveClass('border-[1px]', 'bg-transparent')
|
||||
})
|
||||
|
||||
it('should override color background with hideBg', () => {
|
||||
const { container } = render(<Tag color="red" hideBg>Test</Tag>)
|
||||
const tag = container.firstChild
|
||||
expect(tag).toHaveClass('bg-transparent', 'text-red-800')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Custom Styling', () => {
|
||||
it('should merge custom className', () => {
|
||||
const { container } = render(<Tag className="my-custom-class">Test</Tag>)
|
||||
expect(container.firstChild).toHaveClass('my-custom-class')
|
||||
})
|
||||
|
||||
it('should preserve base classes with custom className', () => {
|
||||
const { container } = render(<Tag className="my-custom-class">Test</Tag>)
|
||||
expect(container.firstChild).toHaveClass('inline-flex', 'my-custom-class')
|
||||
})
|
||||
|
||||
it('should handle empty className prop', () => {
|
||||
const { container } = render(<Tag className="">Test</Tag>)
|
||||
expect(container.firstChild).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
Loading…
Reference in New Issue