mirror of
https://github.com/langgenius/dify.git
synced 2026-11-10 21:35:45 +08:00
test(web): exercise foundational inputs in Browser Mode (#45484)
This commit is contained in:
1 parent
8e67d43735
commit
6e85fa171b
7 files changed
+484
-461
No files matched your search
@@ -0,0 +1,40 @@
|
||||
import { useState } from 'react'
|
||||
import { page, userEvent } from 'vite-plus/test/browser'
|
||||
import { render } from 'vitest-browser-react'
|
||||
import AutoHeightTextarea from '../index'
|
||||
|
||||
function Editor() {
|
||||
const [value, setValue] = useState('hello')
|
||||
const [controlFocus, setControlFocus] = useState(1)
|
||||
return (
|
||||
<>
|
||||
<AutoHeightTextarea
|
||||
aria-label="Message"
|
||||
value={value}
|
||||
onChange={(event) => setValue(event.target.value)}
|
||||
controlFocus={controlFocus}
|
||||
/>
|
||||
<button
|
||||
onClick={() => {
|
||||
setValue('hello world')
|
||||
setControlFocus(2)
|
||||
}}
|
||||
>
|
||||
Replace message
|
||||
</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
it('focuses the replacement text at its end when the caller requests focus', async () => {
|
||||
await page.viewport(800, 600)
|
||||
await render(<Editor />)
|
||||
const input = page.getByRole('textbox', { name: 'Message' })
|
||||
await expect.element(input).toHaveFocus()
|
||||
await userEvent.keyboard('!')
|
||||
await expect.element(input).toHaveValue('hello!')
|
||||
await page.getByRole('button', { name: 'Replace message' }).click()
|
||||
await expect.element(input).toHaveFocus()
|
||||
await userEvent.keyboard('!')
|
||||
await expect.element(input).toHaveValue('hello world!')
|
||||
})
|
||||
@@ -1,22 +0,0 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import AutoHeightTextarea from '../index'
|
||||
|
||||
describe('AutoHeightTextarea', () => {
|
||||
it('moves focus and the cursor to the end when controlFocus changes', () => {
|
||||
const { rerender } = render(
|
||||
<AutoHeightTextarea value="hello" onChange={vi.fn()} controlFocus={1} />,
|
||||
)
|
||||
|
||||
const input = screen.getByRole('textbox') as HTMLTextAreaElement
|
||||
expect(input).toHaveFocus()
|
||||
expect(input.selectionStart).toBe(5)
|
||||
expect(input.selectionEnd).toBe(5)
|
||||
input.blur()
|
||||
|
||||
rerender(<AutoHeightTextarea value="hello world" onChange={vi.fn()} controlFocus={2} />)
|
||||
|
||||
expect(input).toHaveFocus()
|
||||
expect(input.selectionStart).toBe(11)
|
||||
expect(input.selectionEnd).toBe(11)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,226 @@
|
||||
import type { ComponentProps } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { page as screen, userEvent } from 'vite-plus/test/browser'
|
||||
import { render } from 'vitest-browser-react'
|
||||
import { CheckboxList } from '..'
|
||||
|
||||
function ControlledList(props: ComponentProps<typeof CheckboxList>) {
|
||||
const [value, setValue] = useState(props.value ?? [])
|
||||
return (
|
||||
<>
|
||||
<CheckboxList
|
||||
{...props}
|
||||
value={value}
|
||||
onChange={(next) => {
|
||||
setValue(next)
|
||||
}}
|
||||
/>
|
||||
<output aria-label="Selected values">{JSON.stringify(value)}</output>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
describe('checkbox list component', () => {
|
||||
beforeEach(async () => {
|
||||
await screen.viewport(800, 600)
|
||||
})
|
||||
const selectAllName = 'common.operation.selectAll'
|
||||
const getSearchInput = () => screen.getByRole('searchbox', { name: 'common.operation.search' })
|
||||
const options = [
|
||||
{ label: 'Option 1', value: 'option1' },
|
||||
{ label: 'Option 2', value: 'option2' },
|
||||
{ label: 'Option 3', value: 'option3' },
|
||||
{ label: 'Apple', value: 'apple' },
|
||||
]
|
||||
|
||||
it('renders with title, description and options', async () => {
|
||||
await render(
|
||||
<ControlledList title="Test Title" description="Test Description" options={options} />,
|
||||
)
|
||||
await expect.element(screen.getByText('Test Title')).toBeInTheDocument()
|
||||
await expect.element(screen.getByText('Test Description')).toBeInTheDocument()
|
||||
await expect
|
||||
.element(screen.getByRole('group', { name: 'Test Title' }))
|
||||
.toHaveAccessibleDescription('Test Description')
|
||||
for (const option of options) {
|
||||
await expect.element(screen.getByText(option.label)).toBeVisible()
|
||||
}
|
||||
})
|
||||
|
||||
it('filters options by label', async () => {
|
||||
await render(<ControlledList options={options} />)
|
||||
|
||||
const input = getSearchInput()
|
||||
await input.fill('app')
|
||||
|
||||
await expect.element(screen.getByText('Apple')).toBeInTheDocument()
|
||||
await expect.element(screen.getByText('Option 2')).not.toBeInTheDocument()
|
||||
await expect.element(screen.getByText('Option 3')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('selects all options when select-all is clicked', async () => {
|
||||
await render(<ControlledList options={options} value={[]} showSelectAll />)
|
||||
|
||||
const selectAll = screen.getByRole('checkbox', { name: selectAllName })
|
||||
await selectAll.click()
|
||||
|
||||
await expect
|
||||
.element(screen.getByRole('status', { name: 'Selected values' }))
|
||||
.toHaveTextContent(JSON.stringify(['option1', 'option2', 'option3', 'apple']))
|
||||
await expect.element(selectAll).toBeChecked()
|
||||
await userEvent.keyboard(' ')
|
||||
await expect.element(selectAll).not.toBeChecked()
|
||||
await expect.element(screen.getByRole('checkbox', { name: 'Option 1' })).not.toBeChecked()
|
||||
await expect
|
||||
.element(screen.getByRole('status', { name: 'Selected values' }))
|
||||
.toHaveTextContent('[]')
|
||||
})
|
||||
|
||||
it('deselects all options when select-all is clicked', async () => {
|
||||
await render(
|
||||
<ControlledList
|
||||
options={options}
|
||||
value={['option1', 'option2', 'option3', 'apple']}
|
||||
showSelectAll
|
||||
/>,
|
||||
)
|
||||
|
||||
const selectAll = screen.getByRole('checkbox', { name: selectAllName })
|
||||
await expect.element(selectAll).toBeChecked()
|
||||
await selectAll.click()
|
||||
|
||||
await expect
|
||||
.element(screen.getByRole('status', { name: 'Selected values' }))
|
||||
.toHaveTextContent(JSON.stringify([]))
|
||||
})
|
||||
|
||||
it('hides select-all checkbox when searching', async () => {
|
||||
await render(<ControlledList options={options} />)
|
||||
await getSearchInput().fill('app')
|
||||
await expect
|
||||
.element(screen.getByRole('checkbox', { name: selectAllName }))
|
||||
.not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('selects options when checkbox is clicked', async () => {
|
||||
await render(<ControlledList options={options} value={[]} showSelectAll={false} />)
|
||||
|
||||
const selectOption = screen.getByRole('checkbox', { name: 'Option 1' })
|
||||
await selectOption.click()
|
||||
await expect.element(selectOption).toBeChecked()
|
||||
await expect
|
||||
.element(screen.getByRole('status', { name: 'Selected values' }))
|
||||
.toHaveTextContent(JSON.stringify(['option1']))
|
||||
})
|
||||
|
||||
it('deselects options when checkbox is clicked when selected', async () => {
|
||||
await render(<ControlledList options={options} value={['option1']} showSelectAll={false} />)
|
||||
|
||||
const selectOption = screen.getByRole('checkbox', { name: 'Option 1' })
|
||||
await selectOption.click()
|
||||
await expect
|
||||
.element(screen.getByRole('status', { name: 'Selected values' }))
|
||||
.toHaveTextContent(JSON.stringify([]))
|
||||
})
|
||||
|
||||
it('keeps a disabled list unavailable', async () => {
|
||||
await render(<ControlledList options={options} value={[]} disabled />)
|
||||
|
||||
const selectOption = screen.getByRole('checkbox', { name: 'Option 1' })
|
||||
await expect.element(screen.getByRole('checkbox', { name: selectAllName })).toBeDisabled()
|
||||
await expect.element(selectOption).toBeDisabled()
|
||||
await expect
|
||||
.element(screen.getByRole('status', { name: 'Selected values' }))
|
||||
.toHaveTextContent('[]')
|
||||
})
|
||||
|
||||
it('Reset button works', async () => {
|
||||
await render(<ControlledList options={options} value={[]} />)
|
||||
|
||||
const input = getSearchInput()
|
||||
await input.fill('ban')
|
||||
await expect.element(screen.getByText(/common.operation.noSearchResults/)).toBeVisible()
|
||||
await screen.getByRole('button', { name: 'common.operation.resetKeywords' }).click()
|
||||
await expect.element(input).toHaveValue('')
|
||||
await expect.element(screen.getByRole('checkbox', { name: 'Apple' })).toBeInTheDocument()
|
||||
await expect
|
||||
.element(screen.getByRole('status', { name: 'Selected values' }))
|
||||
.toHaveTextContent('[]')
|
||||
})
|
||||
|
||||
it('keeps disabled options unavailable', async () => {
|
||||
const disabledOptions = [
|
||||
{ label: 'Enabled', value: 'enabled' },
|
||||
{ label: 'Disabled', value: 'disabled', disabled: true },
|
||||
]
|
||||
|
||||
await render(<ControlledList options={disabledOptions} value={[]} />)
|
||||
|
||||
const disabledCheckbox = screen.getByRole('checkbox', { name: 'Disabled' })
|
||||
await expect.element(disabledCheckbox).toBeDisabled()
|
||||
await expect
|
||||
.element(screen.getByRole('status', { name: 'Selected values' }))
|
||||
.toHaveTextContent('[]')
|
||||
})
|
||||
|
||||
it('renders with label prop', async () => {
|
||||
await render(<ControlledList options={options} label="Test Label" />)
|
||||
await expect.element(screen.getByText('Test Label')).toBeInTheDocument()
|
||||
await expect.element(screen.getByRole('group', { name: 'Test Label' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders without showSelectAll, showCount, showSearch', async () => {
|
||||
await render(
|
||||
<ControlledList
|
||||
options={options}
|
||||
showSelectAll={false}
|
||||
showCount={false}
|
||||
showSearch={false}
|
||||
/>,
|
||||
)
|
||||
await expect
|
||||
.element(screen.getByRole('checkbox', { name: selectAllName }))
|
||||
.not.toBeInTheDocument()
|
||||
for (const option of options) {
|
||||
await expect.element(screen.getByText(option.label)).toBeVisible()
|
||||
}
|
||||
})
|
||||
|
||||
it('shows indeterminate state when some options are selected', async () => {
|
||||
await render(<ControlledList options={options} value={['option1', 'option2']} showSelectAll />)
|
||||
// When some but not all options are selected, clicking select-all should select all remaining options
|
||||
const selectAll = screen.getByRole('checkbox', { name: selectAllName })
|
||||
await expect.element(selectAll).toBeInTheDocument()
|
||||
await expect.element(selectAll).toHaveAttribute('aria-checked', 'mixed')
|
||||
|
||||
await selectAll.click()
|
||||
await expect
|
||||
.element(screen.getByRole('status', { name: 'Selected values' }))
|
||||
.toHaveTextContent(JSON.stringify(['option1', 'option2', 'option3', 'apple']))
|
||||
})
|
||||
|
||||
it('shows no data message when no options match search', async () => {
|
||||
await render(<ControlledList options={options} />)
|
||||
|
||||
const input = getSearchInput()
|
||||
await input.fill('xyz')
|
||||
|
||||
await expect.element(screen.getByText(/common.operation.noSearchResults/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows correct filtered count message when searching', async () => {
|
||||
await render(<ControlledList options={options} title="Items" />)
|
||||
|
||||
const input = getSearchInput()
|
||||
await input.fill('opt')
|
||||
|
||||
await expect
|
||||
.element(screen.getByText(/operation.searchCount/i))
|
||||
.toHaveTextContent('common.operation.searchCount:{"count":3,"content":"Items"}')
|
||||
})
|
||||
|
||||
it('shows no data message when no options are provided', async () => {
|
||||
await render(<ControlledList options={[]} />)
|
||||
await expect.element(screen.getByText('common.noData')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -1,231 +0,0 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { CheckboxList } from '..'
|
||||
|
||||
describe('checkbox list component', () => {
|
||||
const selectAllName = 'common.operation.selectAll'
|
||||
const getSearchInput = () => screen.getByRole('searchbox', { name: 'common.operation.search' })
|
||||
const options = [
|
||||
{ label: 'Option 1', value: 'option1' },
|
||||
{ label: 'Option 2', value: 'option2' },
|
||||
{ label: 'Option 3', value: 'option3' },
|
||||
{ label: 'Apple', value: 'apple' },
|
||||
]
|
||||
|
||||
it('renders with title, description and options', () => {
|
||||
render(<CheckboxList title="Test Title" description="Test Description" options={options} />)
|
||||
expect(screen.getByText('Test Title'))!.toBeInTheDocument()
|
||||
expect(screen.getByText('Test Description'))!.toBeInTheDocument()
|
||||
expect(screen.getByRole('group', { name: 'Test Title' }))!.toHaveAccessibleDescription(
|
||||
'Test Description',
|
||||
)
|
||||
options.forEach((option) => {
|
||||
expect(screen.getByText(option.label))!.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('filters options by label', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<CheckboxList options={options} />)
|
||||
|
||||
const input = getSearchInput()
|
||||
await user.type(input, 'app')
|
||||
|
||||
expect(screen.getByText('Apple'))!.toBeInTheDocument()
|
||||
expect(screen.queryByText('Option 2')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('Option 3')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('selects all options when select-all is clicked', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onChange = vi.fn()
|
||||
|
||||
render(<CheckboxList options={options} value={[]} onChange={onChange} showSelectAll />)
|
||||
|
||||
const selectAll = screen.getByRole('checkbox', { name: selectAllName })
|
||||
await user.click(selectAll)
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith(['option1', 'option2', 'option3', 'apple'])
|
||||
})
|
||||
|
||||
it('does not select all options when select-all is clicked when disabled', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onChange = vi.fn()
|
||||
|
||||
render(<CheckboxList options={options} value={[]} disabled showSelectAll onChange={onChange} />)
|
||||
|
||||
const selectAll = screen.getByRole('checkbox', { name: selectAllName })
|
||||
await user.click(selectAll)
|
||||
|
||||
expect(onChange).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('deselects all options when select-all is clicked', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onChange = vi.fn()
|
||||
|
||||
render(
|
||||
<CheckboxList
|
||||
options={options}
|
||||
value={['option1', 'option2', 'option3', 'apple']}
|
||||
onChange={onChange}
|
||||
showSelectAll
|
||||
/>,
|
||||
)
|
||||
|
||||
const selectAll = screen.getByRole('checkbox', { name: selectAllName })
|
||||
expect(selectAll).toBeChecked()
|
||||
await user.click(selectAll)
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith([])
|
||||
})
|
||||
|
||||
it('hides select-all checkbox when searching', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<CheckboxList options={options} />)
|
||||
await user.type(getSearchInput(), 'app')
|
||||
expect(screen.queryByRole('checkbox', { name: selectAllName })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('selects options when checkbox is clicked', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onChange = vi.fn()
|
||||
|
||||
render(<CheckboxList options={options} value={[]} onChange={onChange} showSelectAll={false} />)
|
||||
|
||||
const selectOption = screen.getByRole('checkbox', { name: 'Option 1' })
|
||||
await user.click(selectOption)
|
||||
expect(onChange).toHaveBeenCalledWith(['option1'])
|
||||
})
|
||||
|
||||
it('deselects options when checkbox is clicked when selected', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onChange = vi.fn()
|
||||
|
||||
render(
|
||||
<CheckboxList
|
||||
options={options}
|
||||
value={['option1']}
|
||||
onChange={onChange}
|
||||
showSelectAll={false}
|
||||
/>,
|
||||
)
|
||||
|
||||
const selectOption = screen.getByRole('checkbox', { name: 'Option 1' })
|
||||
await user.click(selectOption)
|
||||
expect(onChange).toHaveBeenCalledWith([])
|
||||
})
|
||||
|
||||
it('does not select options when checkbox is clicked', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onChange = vi.fn()
|
||||
|
||||
render(<CheckboxList options={options} value={[]} onChange={onChange} disabled />)
|
||||
|
||||
const selectOption = screen.getByRole('checkbox', { name: 'Option 1' })
|
||||
await user.click(selectOption)
|
||||
expect(onChange).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('Reset button works', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onChange = vi.fn()
|
||||
|
||||
render(<CheckboxList options={options} value={[]} onChange={onChange} />)
|
||||
|
||||
const input = getSearchInput()
|
||||
await user.type(input, 'ban')
|
||||
await user.click(screen.getByText('common.operation.resetKeywords'))
|
||||
expect(input).toHaveValue('')
|
||||
expect(screen.getByRole('checkbox', { name: 'Apple' })).toBeInTheDocument()
|
||||
expect(onChange).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('does not toggle disabled option when clicked', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onChange = vi.fn()
|
||||
const disabledOptions = [
|
||||
{ label: 'Enabled', value: 'enabled' },
|
||||
{ label: 'Disabled', value: 'disabled', disabled: true },
|
||||
]
|
||||
|
||||
render(<CheckboxList options={disabledOptions} value={[]} onChange={onChange} />)
|
||||
|
||||
const disabledCheckbox = screen.getByRole('checkbox', { name: 'Disabled' })
|
||||
await user.click(disabledCheckbox)
|
||||
expect(onChange).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('renders with label prop', () => {
|
||||
render(<CheckboxList options={options} label="Test Label" />)
|
||||
expect(screen.getByText('Test Label'))!.toBeInTheDocument()
|
||||
expect(screen.getByRole('group', { name: 'Test Label' }))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders without showSelectAll, showCount, showSearch', () => {
|
||||
render(
|
||||
<CheckboxList options={options} showSelectAll={false} showCount={false} showSearch={false} />,
|
||||
)
|
||||
expect(screen.queryByRole('checkbox', { name: selectAllName })).not.toBeInTheDocument()
|
||||
options.forEach((option) => {
|
||||
expect(screen.getByText(option.label))!.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('shows indeterminate state when some options are selected', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onChange = vi.fn()
|
||||
render(
|
||||
<CheckboxList
|
||||
options={options}
|
||||
value={['option1', 'option2']}
|
||||
onChange={onChange}
|
||||
showSelectAll
|
||||
/>,
|
||||
)
|
||||
// When some but not all options are selected, clicking select-all should select all remaining options
|
||||
const selectAll = screen.getByRole('checkbox', { name: selectAllName })
|
||||
expect(selectAll)!.toBeInTheDocument()
|
||||
expect(selectAll)!.toHaveAttribute('aria-checked', 'mixed')
|
||||
|
||||
await user.click(selectAll)
|
||||
expect(onChange).toHaveBeenCalledWith(['option1', 'option2', 'option3', 'apple'])
|
||||
})
|
||||
|
||||
it('shows no data message when no options match search', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<CheckboxList options={options} />)
|
||||
|
||||
const input = getSearchInput()
|
||||
await user.type(input, 'xyz')
|
||||
|
||||
expect(screen.getByText(/common.operation.noSearchResults/i))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('does not toggle when clicking disabled option row', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onChange = vi.fn()
|
||||
const disabledOptions = [{ label: 'Option 1', value: 'option1', disabled: true }]
|
||||
|
||||
render(<CheckboxList options={disabledOptions} value={[]} onChange={onChange} />)
|
||||
|
||||
await user.click(screen.getByText('Option 1'))
|
||||
|
||||
expect(onChange).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('shows correct filtered count message when searching', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<CheckboxList options={options} title="Items" />)
|
||||
|
||||
const input = getSearchInput()
|
||||
await user.type(input, 'opt')
|
||||
|
||||
expect(screen.getByText(/operation.searchCount/i)).toHaveTextContent('3')
|
||||
})
|
||||
|
||||
it('shows no data message when no options are provided', () => {
|
||||
render(<CheckboxList options={[]} />)
|
||||
expect(screen.getByText('common.noData'))!.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
+20
-13
@@ -1,8 +1,8 @@
|
||||
import { RadioGroup } from '@langgenius/dify-ui/radio-group'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { useState } from 'react'
|
||||
import { describe, expect, it } from 'vite-plus/test'
|
||||
import { page as screen, userEvent } from 'vite-plus/test/browser'
|
||||
import { render } from 'vitest-browser-react'
|
||||
import RadioCard from '../index'
|
||||
|
||||
type ExampleMode = 'standard' | 'advanced'
|
||||
@@ -46,18 +46,25 @@ function SelectableCards() {
|
||||
}
|
||||
|
||||
describe('RadioCard', () => {
|
||||
beforeEach(async () => {
|
||||
await screen.viewport(800, 600)
|
||||
})
|
||||
it('connects card selection to the controlled group', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<SelectableCards />)
|
||||
await render(<SelectableCards />)
|
||||
const advanced = screen.getByRole('radio', { name: /Advanced/ })
|
||||
expect(advanced).not.toBeChecked()
|
||||
await user.click(advanced)
|
||||
expect(advanced).toBeChecked()
|
||||
expect(screen.getByRole('radio', { name: /Standard/ })).not.toBeChecked()
|
||||
await expect.element(advanced).not.toBeChecked()
|
||||
await expect.element(screen.getByText('Advanced config')).not.toBeVisible()
|
||||
await advanced.click()
|
||||
await expect.element(advanced).toBeChecked()
|
||||
await expect.element(screen.getByText('Advanced config')).toBeVisible()
|
||||
await expect.element(screen.getByRole('radio', { name: /Standard/ })).not.toBeChecked()
|
||||
await userEvent.keyboard('{ArrowLeft}')
|
||||
await expect.element(screen.getByRole('radio', { name: /Standard/ })).toBeChecked()
|
||||
await expect.element(screen.getByText('Advanced config')).not.toBeVisible()
|
||||
})
|
||||
|
||||
it('should render noRadio card as static content without radio role', () => {
|
||||
render(
|
||||
it('should render noRadio card as static content without radio role', async () => {
|
||||
await render(
|
||||
<RadioCard
|
||||
noRadio
|
||||
icon={<span>i</span>}
|
||||
@@ -67,8 +74,8 @@ describe('RadioCard', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText('No Radio')).toBeInTheDocument()
|
||||
expect(screen.getByText('Static config')).toBeInTheDocument()
|
||||
expect(screen.queryByRole('radio')).not.toBeInTheDocument()
|
||||
await expect.element(screen.getByText('No Radio')).toBeInTheDocument()
|
||||
await expect.element(screen.getByText('Static config')).toBeInTheDocument()
|
||||
await expect.element(screen.getByRole('radio')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,198 @@
|
||||
import type { ComponentProps } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { page as screen, userEvent } from 'vite-plus/test/browser'
|
||||
import { render } from 'vitest-browser-react'
|
||||
import TagInput from '../index'
|
||||
|
||||
const mockNotify = vi.fn()
|
||||
|
||||
vi.mock('@/app/notifications', () => ({
|
||||
toast: { error: (message: string) => mockNotify({ type: 'error', message }) },
|
||||
}))
|
||||
|
||||
type TagInputProps = ComponentProps<typeof TagInput>
|
||||
|
||||
function ControlledTags(props: Partial<TagInputProps>) {
|
||||
const [items, setItems] = useState(props.items ?? [])
|
||||
return (
|
||||
<>
|
||||
<label htmlFor="recipient-tags">Recipients</label>
|
||||
<TagInput {...props} id="recipient-tags" items={items} onChange={setItems} />
|
||||
<output aria-label="Tags">{JSON.stringify(items)}</output>
|
||||
<button>Next field</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const renderTagInput = async (props: Partial<TagInputProps> = {}) => {
|
||||
await render(<ControlledTags {...props} />)
|
||||
}
|
||||
|
||||
describe('TagInput', () => {
|
||||
beforeEach(async () => {
|
||||
await screen.viewport(800, 600)
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should hide input when add is disabled', async () => {
|
||||
await renderTagInput({ disableAdd: true })
|
||||
|
||||
await expect.element(screen.getByRole('textbox')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should hide remove controls when remove is disabled', async () => {
|
||||
await renderTagInput({ items: ['alpha'], disableRemove: true })
|
||||
|
||||
await expect
|
||||
.element(screen.getByRole('button', { name: 'common.operation.remove alpha' }))
|
||||
.not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('associates an external label with the tag entry input', async () => {
|
||||
await renderTagInput()
|
||||
|
||||
const input = screen.getByRole('textbox', { name: 'Recipients' })
|
||||
await screen.getByText('Recipients').click()
|
||||
await expect.element(input).toHaveFocus()
|
||||
await userEvent.keyboard('alice')
|
||||
await expect.element(input).toHaveValue('alice')
|
||||
await userEvent.keyboard('{Enter}')
|
||||
await expect
|
||||
.element(screen.getByRole('status', { name: 'Tags' }))
|
||||
.toHaveTextContent('["alice"]')
|
||||
})
|
||||
|
||||
describe('User Interactions', () => {
|
||||
it('should remove item when remove control is clicked', async () => {
|
||||
await renderTagInput({ items: ['alpha', 'beta'] })
|
||||
|
||||
const removeControl = screen.getByRole('button', { name: 'common.operation.remove alpha' })
|
||||
|
||||
await removeControl.click()
|
||||
await expect.element(removeControl).not.toBeInTheDocument()
|
||||
await expect
|
||||
.element(screen.getByRole('button', { name: 'common.operation.remove beta' }))
|
||||
.toBeVisible()
|
||||
|
||||
await expect
|
||||
.element(screen.getByRole('status', { name: 'Tags' }))
|
||||
.toHaveTextContent(JSON.stringify(['beta']))
|
||||
})
|
||||
|
||||
it('should add trimmed tag on Enter and clear input', async () => {
|
||||
await renderTagInput()
|
||||
const input = screen.getByRole('textbox')
|
||||
|
||||
await input.fill(' new-tag ')
|
||||
await userEvent.keyboard('{Enter}')
|
||||
|
||||
await expect
|
||||
.element(screen.getByRole('status', { name: 'Tags' }))
|
||||
.toHaveTextContent(JSON.stringify(['new-tag']))
|
||||
await expect.element(input).toHaveValue('')
|
||||
await expect
|
||||
.element(screen.getByRole('button', { name: 'common.operation.remove new-tag' }))
|
||||
.toBeVisible()
|
||||
})
|
||||
|
||||
it('should add tag on blur when input has valid value', async () => {
|
||||
await renderTagInput()
|
||||
const input = screen.getByRole('textbox')
|
||||
|
||||
await input.fill('blur-tag')
|
||||
await screen.getByRole('button', { name: 'Next field' }).click()
|
||||
|
||||
await expect
|
||||
.element(screen.getByRole('status', { name: 'Tags' }))
|
||||
.toHaveTextContent(JSON.stringify(['blur-tag']))
|
||||
})
|
||||
|
||||
it('should append return marker on Enter and confirm on Tab in special mode', async () => {
|
||||
await renderTagInput({ customizedConfirmKey: 'Tab' })
|
||||
const input = screen.getByRole('textbox')
|
||||
|
||||
await input.fill('stop')
|
||||
await userEvent.keyboard('{Enter}')
|
||||
|
||||
await expect.element(input).toHaveValue('stop↵')
|
||||
await expect
|
||||
.element(screen.getByRole('status', { name: 'Tags' }))
|
||||
.toHaveTextContent(JSON.stringify([]))
|
||||
|
||||
await userEvent.keyboard('{Tab}')
|
||||
await expect.element(input).toHaveFocus()
|
||||
await expect
|
||||
.element(screen.getByRole('status', { name: 'Tags' }))
|
||||
.toHaveTextContent(JSON.stringify(['stop↵']))
|
||||
await expect.element(input).toHaveValue('')
|
||||
await expect
|
||||
.element(screen.getByRole('button', { name: 'common.operation.remove stop↵' }))
|
||||
.toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Validation', () => {
|
||||
it('should notify duplicate error when tag already exists', async () => {
|
||||
await renderTagInput({ items: ['dup-tag'] })
|
||||
const input = screen.getByRole('textbox')
|
||||
|
||||
await input.fill('dup-tag')
|
||||
await userEvent.keyboard('{Enter}')
|
||||
|
||||
await expect
|
||||
.element(screen.getByRole('status', { name: 'Tags' }))
|
||||
.toHaveTextContent(JSON.stringify(['dup-tag']))
|
||||
expect(mockNotify).toHaveBeenCalledWith({
|
||||
type: 'error',
|
||||
message: 'datasetDocuments.segment.keywordDuplicate',
|
||||
})
|
||||
})
|
||||
|
||||
it('should notify length error when tag is longer than 20 chars', async () => {
|
||||
await renderTagInput()
|
||||
const input = screen.getByRole('textbox')
|
||||
|
||||
await input.fill('a'.repeat(21))
|
||||
await userEvent.keyboard('{Enter}')
|
||||
|
||||
await expect
|
||||
.element(screen.getByRole('status', { name: 'Tags' }))
|
||||
.toHaveTextContent(JSON.stringify([]))
|
||||
expect(mockNotify).toHaveBeenCalledWith({
|
||||
type: 'error',
|
||||
message: 'datasetDocuments.segment.keywordError',
|
||||
})
|
||||
})
|
||||
|
||||
it('should notify required error when value is empty and required is true', async () => {
|
||||
await renderTagInput({ required: true })
|
||||
const input = screen.getByRole('textbox')
|
||||
|
||||
await input.fill(' ')
|
||||
await screen.getByRole('button', { name: 'Next field' }).click()
|
||||
|
||||
await expect
|
||||
.element(screen.getByRole('status', { name: 'Tags' }))
|
||||
.toHaveTextContent(JSON.stringify([]))
|
||||
expect(mockNotify).toHaveBeenCalledWith({
|
||||
type: 'error',
|
||||
message: 'datasetDocuments.segment.keywordEmpty',
|
||||
})
|
||||
})
|
||||
|
||||
it('should ignore empty value when required is false', async () => {
|
||||
await renderTagInput({ required: false })
|
||||
const input = screen.getByRole('textbox')
|
||||
|
||||
await input.fill(' ')
|
||||
await screen.getByRole('button', { name: 'Next field' }).click()
|
||||
|
||||
await expect
|
||||
.element(screen.getByRole('status', { name: 'Tags' }))
|
||||
.toHaveTextContent(JSON.stringify([]))
|
||||
expect(mockNotify).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,195 +0,0 @@
|
||||
import type { ComponentProps } from 'react'
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import TagInput from '../index'
|
||||
|
||||
const mockNotify = vi.fn()
|
||||
|
||||
vi.mock('@/app/notifications', () => ({
|
||||
default: {
|
||||
notify: (args: unknown) => mockNotify(args),
|
||||
},
|
||||
toast: {
|
||||
success: (message: string) => mockNotify({ type: 'success', message }),
|
||||
error: (message: string) => mockNotify({ type: 'error', message }),
|
||||
warning: (message: string) => mockNotify({ type: 'warning', message }),
|
||||
info: (message: string) => mockNotify({ type: 'info', message }),
|
||||
},
|
||||
}))
|
||||
|
||||
type TagInputProps = ComponentProps<typeof TagInput>
|
||||
|
||||
const renderTagInput = (props: Partial<TagInputProps> = {}) => {
|
||||
const onChange = vi.fn<(items: string[]) => void>()
|
||||
const items = props.items ?? []
|
||||
|
||||
render(<TagInput items={items} onChange={onChange} {...props} />)
|
||||
|
||||
return { onChange }
|
||||
}
|
||||
|
||||
describe('TagInput', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should expose the current input text for CSS sizing', async () => {
|
||||
const user = userEvent.setup()
|
||||
renderTagInput({ placeholder: 'Tag' })
|
||||
const input = screen.getByRole('textbox')
|
||||
const inputContainer = input.parentElement
|
||||
|
||||
expect(inputContainer).toHaveAttribute('data-input-value', 'Tag')
|
||||
|
||||
await user.type(input, 'longer')
|
||||
|
||||
expect(inputContainer).toHaveAttribute('data-input-value', 'longer')
|
||||
})
|
||||
|
||||
it('should hide input when add is disabled', () => {
|
||||
renderTagInput({ disableAdd: true })
|
||||
|
||||
expect(screen.queryByRole('textbox')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should hide remove controls when remove is disabled', () => {
|
||||
renderTagInput({ items: ['alpha'], disableRemove: true })
|
||||
|
||||
expect(
|
||||
screen.queryByRole('button', { name: 'common.operation.remove alpha' }),
|
||||
).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('associates an external label with the tag entry input', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onChange = vi.fn()
|
||||
render(
|
||||
<>
|
||||
<label htmlFor="recipient-tags">Recipients</label>
|
||||
<TagInput id="recipient-tags" items={[]} onChange={onChange} />
|
||||
</>,
|
||||
)
|
||||
|
||||
const input = screen.getByRole('textbox', { name: 'Recipients' })
|
||||
await user.click(screen.getByText('Recipients'))
|
||||
expect(input).toHaveFocus()
|
||||
await user.keyboard('alice{Enter}')
|
||||
expect(onChange).toHaveBeenCalledWith(['alice'])
|
||||
})
|
||||
|
||||
describe('User Interactions', () => {
|
||||
it('should remove item when remove control is clicked', async () => {
|
||||
const user = userEvent.setup()
|
||||
const { onChange } = renderTagInput({ items: ['alpha', 'beta'] })
|
||||
|
||||
const removeControl = screen.getByRole('button', { name: 'common.operation.remove alpha' })
|
||||
|
||||
await user.click(removeControl)
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith(['beta'])
|
||||
})
|
||||
|
||||
it('should add trimmed tag on Enter and clear input', async () => {
|
||||
const user = userEvent.setup()
|
||||
const { onChange } = renderTagInput()
|
||||
const input = screen.getByRole('textbox')
|
||||
|
||||
await user.type(input, ' new-tag ')
|
||||
await user.type(input, '{Enter}')
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith(['new-tag'])
|
||||
await waitFor(() => {
|
||||
expect(input)!.toHaveValue('')
|
||||
})
|
||||
})
|
||||
|
||||
it('should add tag on blur when input has valid value', async () => {
|
||||
const user = userEvent.setup()
|
||||
const { onChange } = renderTagInput()
|
||||
const input = screen.getByRole('textbox')
|
||||
|
||||
await user.type(input, 'blur-tag')
|
||||
await user.click(document.body)
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith(['blur-tag'])
|
||||
})
|
||||
|
||||
it('should append return marker on Enter and confirm on Tab in special mode', async () => {
|
||||
const user = userEvent.setup()
|
||||
const { onChange } = renderTagInput({ customizedConfirmKey: 'Tab' })
|
||||
const input = screen.getByRole('textbox')
|
||||
|
||||
// Type normally
|
||||
await user.type(input, 'stop')
|
||||
await user.keyboard('{Enter}')
|
||||
|
||||
expect(input)!.toHaveValue('stop↵')
|
||||
expect(onChange).not.toHaveBeenCalled()
|
||||
|
||||
await user.keyboard('{Tab}')
|
||||
expect(input).toHaveFocus()
|
||||
expect(onChange).toHaveBeenCalledWith(['stop↵'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('Validation', () => {
|
||||
it('should notify duplicate error when tag already exists', async () => {
|
||||
const user = userEvent.setup()
|
||||
const { onChange } = renderTagInput({ items: ['dup-tag'] })
|
||||
const input = screen.getByRole('textbox')
|
||||
|
||||
await user.type(input, 'dup-tag')
|
||||
await user.keyboard('{Enter}')
|
||||
|
||||
expect(onChange).not.toHaveBeenCalled()
|
||||
expect(mockNotify).toHaveBeenCalledWith({
|
||||
type: 'error',
|
||||
message: 'datasetDocuments.segment.keywordDuplicate',
|
||||
})
|
||||
})
|
||||
|
||||
it('should notify length error when tag is longer than 20 chars', async () => {
|
||||
const user = userEvent.setup()
|
||||
const { onChange } = renderTagInput()
|
||||
const input = screen.getByRole('textbox')
|
||||
|
||||
await user.type(input, 'a'.repeat(21))
|
||||
await user.keyboard('{Enter}')
|
||||
|
||||
expect(onChange).not.toHaveBeenCalled()
|
||||
expect(mockNotify).toHaveBeenCalledWith({
|
||||
type: 'error',
|
||||
message: 'datasetDocuments.segment.keywordError',
|
||||
})
|
||||
})
|
||||
|
||||
it('should notify required error when value is empty and required is true', async () => {
|
||||
const user = userEvent.setup()
|
||||
const { onChange } = renderTagInput({ required: true })
|
||||
const input = screen.getByRole('textbox')
|
||||
|
||||
await user.type(input, ' ')
|
||||
await user.click(document.body)
|
||||
|
||||
expect(onChange).not.toHaveBeenCalled()
|
||||
expect(mockNotify).toHaveBeenCalledWith({
|
||||
type: 'error',
|
||||
message: 'datasetDocuments.segment.keywordEmpty',
|
||||
})
|
||||
})
|
||||
|
||||
it('should ignore empty value when required is false', async () => {
|
||||
const user = userEvent.setup()
|
||||
const { onChange } = renderTagInput({ required: false })
|
||||
const input = screen.getByRole('textbox')
|
||||
|
||||
await user.type(input, ' ')
|
||||
await user.click(document.body)
|
||||
|
||||
expect(onChange).not.toHaveBeenCalled()
|
||||
expect(mockNotify).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user