mirror of
https://github.com/langgenius/dify.git
synced 2026-04-30 05:06:29 +08:00
Signed-off-by: majiayu000 <1835304752@qq.com> Signed-off-by: dependabot[bot] <support@github.com> Signed-off-by: NeatGuyCoding <15627489+NeatGuyCoding@users.noreply.github.com> Signed-off-by: -LAN- <laipz8200@outlook.com> Signed-off-by: yihong0618 <zouzou0208@gmail.com> Co-authored-by: QuantumGhost <obelisk.reg+git@gmail.com> Co-authored-by: 盐粒 Yanli <yanli@dify.ai> Co-authored-by: wangxiaolei <fatelei@gmail.com> Co-authored-by: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Co-authored-by: Cursx <33718736+Cursx@users.noreply.github.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: lif <1835304752@qq.com> Co-authored-by: 非法操作 <hjlarry@163.com> Co-authored-by: Asuka Minato <i@asukaminato.eu.org> Co-authored-by: fenglin <790872612@qq.com> Co-authored-by: qiaofenglin <qiaofenglin@baidu.com> Co-authored-by: -LAN- <laipz8200@outlook.com> Co-authored-by: TomoOkuyama <49631611+TomoOkuyama@users.noreply.github.com> Co-authored-by: Tomo Okuyama <tomo.okuyama@intersystems.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: zyssyz123 <916125788@qq.com> Co-authored-by: hj24 <mambahj24@gmail.com> Co-authored-by: Coding On Star <447357187@qq.com> Co-authored-by: CodingOnStar <hanxujiang@dify.ai> Co-authored-by: yyh <92089059+lyzno1@users.noreply.github.com> Co-authored-by: Xiangxuan Qu <fghpdf@outlook.com> Co-authored-by: fghpdf <fghpdf@users.noreply.github.com> Co-authored-by: coopercoder <whitetiger0127@163.com> Co-authored-by: zhaiguangpeng <zhaiguangpeng@didiglobal.com> Co-authored-by: Junyan Qin (Chin) <rockchinq@gmail.com> Co-authored-by: E.G <146701565+GlobalStar117@users.noreply.github.com> Co-authored-by: GlobalStar117 <GlobalStar117@users.noreply.github.com> Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com> Co-authored-by: CodingOnStar <hanxujiang@dify.com> Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com> Co-authored-by: heyszt <270985384@qq.com> Co-authored-by: NeatGuyCoding <15627489+NeatGuyCoding@users.noreply.github.com> Co-authored-by: Yeuoly <45712896+Yeuoly@users.noreply.github.com> Co-authored-by: zxhlyh <jasonapring2015@outlook.com> Co-authored-by: moonpanda <chuanzegao@163.com> Co-authored-by: warlocgao <warlocgao@tencent.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: KVOJJJin <jzongcode@gmail.com> Co-authored-by: eux <euxx@users.noreply.github.com> Co-authored-by: bangjiehan <bangjiehan@gmail.com> Co-authored-by: FFXN <31929997+FFXN@users.noreply.github.com> Co-authored-by: Jyong <76649700+JohnJyong@users.noreply.github.com> Co-authored-by: Nie Ronghua <nieronghua@sf-express.com> Co-authored-by: JQSevenMiao <141806521+JQSevenMiao@users.noreply.github.com> Co-authored-by: jiasiqi <jiasiqi3@tal.com> Co-authored-by: Seokrin Taron Sung <sungsjade@gmail.com> Co-authored-by: CrabSAMA <40541269+CrabSAMA@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: yihong <zouzou0208@gmail.com> Co-authored-by: Joel <iamjoel007@gmail.com> Co-authored-by: Wu Tianwei <30284043+WTW0313@users.noreply.github.com> Co-authored-by: yessenia <yessenia.contact@gmail.com> Co-authored-by: Jax <anobaka@qq.com> Co-authored-by: niveshdandyan <155956228+niveshdandyan@users.noreply.github.com> Co-authored-by: OSS Contributor <oss-contributor@example.com> Co-authored-by: niveshdandyan <niveshdandyan@users.noreply.github.com> Co-authored-by: Sean Kenneth Doherty <Smaster7772@gmail.com>
315 lines
11 KiB
TypeScript
315 lines
11 KiB
TypeScript
import { act, render, screen } from '@testing-library/react'
|
|
import userEvent from '@testing-library/user-event'
|
|
import copy from 'copy-to-clipboard'
|
|
import InputCopy from './input-copy'
|
|
|
|
// Mock copy-to-clipboard
|
|
vi.mock('copy-to-clipboard', () => ({
|
|
default: vi.fn().mockReturnValue(true),
|
|
}))
|
|
|
|
describe('InputCopy', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
vi.useFakeTimers({ shouldAdvanceTime: true })
|
|
})
|
|
|
|
afterEach(() => {
|
|
vi.runOnlyPendingTimers()
|
|
vi.useRealTimers()
|
|
})
|
|
|
|
describe('rendering', () => {
|
|
it('should render the value', () => {
|
|
render(<InputCopy value="test-api-key-12345" />)
|
|
expect(screen.getByText('test-api-key-12345')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render with empty value by default', () => {
|
|
render(<InputCopy />)
|
|
// Empty string should be rendered
|
|
expect(screen.getByRole('button')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render children when provided', () => {
|
|
render(
|
|
<InputCopy value="key">
|
|
<span data-testid="custom-child">Custom Content</span>
|
|
</InputCopy>,
|
|
)
|
|
expect(screen.getByTestId('custom-child')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render CopyFeedback component', () => {
|
|
render(<InputCopy value="test" />)
|
|
// CopyFeedback should render a button
|
|
const buttons = screen.getAllByRole('button')
|
|
expect(buttons.length).toBeGreaterThan(0)
|
|
})
|
|
})
|
|
|
|
describe('styling', () => {
|
|
it('should apply custom className', () => {
|
|
const { container } = render(<InputCopy value="test" className="custom-class" />)
|
|
const wrapper = container.firstChild as HTMLElement
|
|
expect(wrapper.className).toContain('custom-class')
|
|
})
|
|
|
|
it('should have flex layout', () => {
|
|
const { container } = render(<InputCopy value="test" />)
|
|
const wrapper = container.firstChild as HTMLElement
|
|
expect(wrapper.className).toContain('flex')
|
|
})
|
|
|
|
it('should have items-center alignment', () => {
|
|
const { container } = render(<InputCopy value="test" />)
|
|
const wrapper = container.firstChild as HTMLElement
|
|
expect(wrapper.className).toContain('items-center')
|
|
})
|
|
|
|
it('should have rounded-lg class', () => {
|
|
const { container } = render(<InputCopy value="test" />)
|
|
const wrapper = container.firstChild as HTMLElement
|
|
expect(wrapper.className).toContain('rounded-lg')
|
|
})
|
|
|
|
it('should have background class', () => {
|
|
const { container } = render(<InputCopy value="test" />)
|
|
const wrapper = container.firstChild as HTMLElement
|
|
expect(wrapper.className).toContain('bg-components-input-bg-normal')
|
|
})
|
|
|
|
it('should have hover state', () => {
|
|
const { container } = render(<InputCopy value="test" />)
|
|
const wrapper = container.firstChild as HTMLElement
|
|
expect(wrapper.className).toContain('hover:bg-state-base-hover')
|
|
})
|
|
|
|
it('should have py-2 padding', () => {
|
|
const { container } = render(<InputCopy value="test" />)
|
|
const wrapper = container.firstChild as HTMLElement
|
|
expect(wrapper.className).toContain('py-2')
|
|
})
|
|
})
|
|
|
|
describe('copy functionality', () => {
|
|
it('should copy value when clicked', async () => {
|
|
const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime })
|
|
render(<InputCopy value="copy-this-value" />)
|
|
|
|
const copyableArea = screen.getByText('copy-this-value')
|
|
await act(async () => {
|
|
await user.click(copyableArea)
|
|
})
|
|
|
|
expect(copy).toHaveBeenCalledWith('copy-this-value')
|
|
})
|
|
|
|
it('should update copied state after clicking', async () => {
|
|
const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime })
|
|
render(<InputCopy value="test-value" />)
|
|
|
|
const copyableArea = screen.getByText('test-value')
|
|
await act(async () => {
|
|
await user.click(copyableArea)
|
|
})
|
|
|
|
// Copy function should have been called
|
|
expect(copy).toHaveBeenCalledWith('test-value')
|
|
})
|
|
|
|
it('should reset copied state after timeout', async () => {
|
|
const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime })
|
|
render(<InputCopy value="test-value" />)
|
|
|
|
const copyableArea = screen.getByText('test-value')
|
|
await act(async () => {
|
|
await user.click(copyableArea)
|
|
})
|
|
|
|
expect(copy).toHaveBeenCalledWith('test-value')
|
|
|
|
// Advance time to reset the copied state
|
|
await act(async () => {
|
|
vi.advanceTimersByTime(1500)
|
|
})
|
|
|
|
// Component should still be functional
|
|
expect(screen.getByText('test-value')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render tooltip on value', () => {
|
|
render(<InputCopy value="test-value" />)
|
|
// Value should be wrapped in tooltip (tooltip shows on hover, not as visible text)
|
|
const valueText = screen.getByText('test-value')
|
|
expect(valueText).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('tooltip', () => {
|
|
it('should render tooltip wrapper', () => {
|
|
render(<InputCopy value="test" />)
|
|
const valueText = screen.getByText('test')
|
|
expect(valueText).toBeInTheDocument()
|
|
})
|
|
|
|
it('should have cursor-pointer on clickable area', () => {
|
|
render(<InputCopy value="test" />)
|
|
const valueText = screen.getByText('test')
|
|
const clickableArea = valueText.closest('div[class*="cursor-pointer"]')
|
|
expect(clickableArea).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('divider', () => {
|
|
it('should render vertical divider', () => {
|
|
const { container } = render(<InputCopy value="test" />)
|
|
const divider = container.querySelector('.bg-divider-regular')
|
|
expect(divider).toBeInTheDocument()
|
|
})
|
|
|
|
it('should have correct divider dimensions', () => {
|
|
const { container } = render(<InputCopy value="test" />)
|
|
const divider = container.querySelector('.bg-divider-regular')
|
|
expect(divider?.className).toContain('h-4')
|
|
expect(divider?.className).toContain('w-px')
|
|
})
|
|
|
|
it('should have shrink-0 on divider', () => {
|
|
const { container } = render(<InputCopy value="test" />)
|
|
const divider = container.querySelector('.bg-divider-regular')
|
|
expect(divider?.className).toContain('shrink-0')
|
|
})
|
|
})
|
|
|
|
describe('value display', () => {
|
|
it('should have truncate class for long values', () => {
|
|
render(<InputCopy value="very-long-api-key-value-that-might-overflow" />)
|
|
const valueText = screen.getByText('very-long-api-key-value-that-might-overflow')
|
|
const container = valueText.closest('div[class*="truncate"]')
|
|
expect(container).toBeInTheDocument()
|
|
})
|
|
|
|
it('should have text-secondary color on value', () => {
|
|
render(<InputCopy value="test-value" />)
|
|
const valueText = screen.getByText('test-value')
|
|
expect(valueText.className).toContain('text-text-secondary')
|
|
})
|
|
|
|
it('should have absolute positioning for overlay', () => {
|
|
render(<InputCopy value="test" />)
|
|
const valueText = screen.getByText('test')
|
|
const container = valueText.closest('div[class*="absolute"]')
|
|
expect(container).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('inner container', () => {
|
|
it('should have grow class on inner container', () => {
|
|
const { container } = render(<InputCopy value="test" />)
|
|
const innerContainer = container.querySelector('.grow')
|
|
expect(innerContainer).toBeInTheDocument()
|
|
})
|
|
|
|
it('should have h-5 height on inner container', () => {
|
|
const { container } = render(<InputCopy value="test" />)
|
|
const innerContainer = container.querySelector('.h-5')
|
|
expect(innerContainer).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('with children', () => {
|
|
it('should render children before value', () => {
|
|
const { container } = render(
|
|
<InputCopy value="key">
|
|
<span data-testid="prefix">Prefix:</span>
|
|
</InputCopy>,
|
|
)
|
|
const children = container.querySelector('[data-testid="prefix"]')
|
|
expect(children).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render both children and value', () => {
|
|
render(
|
|
<InputCopy value="api-key">
|
|
<span>Label:</span>
|
|
</InputCopy>,
|
|
)
|
|
expect(screen.getByText('Label:')).toBeInTheDocument()
|
|
expect(screen.getByText('api-key')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('CopyFeedback section', () => {
|
|
it('should have margin on CopyFeedback container', () => {
|
|
const { container } = render(<InputCopy value="test" />)
|
|
const copyFeedbackContainer = container.querySelector('.mx-1')
|
|
expect(copyFeedbackContainer).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('relative container', () => {
|
|
it('should have relative positioning on value container', () => {
|
|
const { container } = render(<InputCopy value="test" />)
|
|
const relativeContainer = container.querySelector('.relative')
|
|
expect(relativeContainer).toBeInTheDocument()
|
|
})
|
|
|
|
it('should have grow on value container', () => {
|
|
const { container } = render(<InputCopy value="test" />)
|
|
// Find the relative container that also has grow
|
|
const valueContainer = container.querySelector('.relative.grow')
|
|
expect(valueContainer).toBeInTheDocument()
|
|
})
|
|
|
|
it('should have full height on value container', () => {
|
|
const { container } = render(<InputCopy value="test" />)
|
|
const valueContainer = container.querySelector('.relative.h-full')
|
|
expect(valueContainer).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('edge cases', () => {
|
|
it('should handle undefined value', () => {
|
|
render(<InputCopy value={undefined} />)
|
|
// Should not crash
|
|
expect(screen.getByRole('button')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should handle empty string value', () => {
|
|
render(<InputCopy value="" />)
|
|
expect(screen.getByRole('button')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should handle very long values', () => {
|
|
const longValue = 'a'.repeat(500)
|
|
render(<InputCopy value={longValue} />)
|
|
expect(screen.getByText(longValue)).toBeInTheDocument()
|
|
})
|
|
|
|
it('should handle special characters in value', () => {
|
|
const specialValue = 'key-with-special-chars!@#$%^&*()'
|
|
render(<InputCopy value={specialValue} />)
|
|
expect(screen.getByText(specialValue)).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('multiple clicks', () => {
|
|
it('should handle multiple rapid clicks', async () => {
|
|
const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime })
|
|
render(<InputCopy value="test" />)
|
|
|
|
const copyableArea = screen.getByText('test')
|
|
|
|
// Click multiple times rapidly
|
|
await act(async () => {
|
|
await user.click(copyableArea)
|
|
await user.click(copyableArea)
|
|
await user.click(copyableArea)
|
|
})
|
|
|
|
expect(copy).toHaveBeenCalledTimes(3)
|
|
})
|
|
})
|
|
})
|