Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
184 changes: 184 additions & 0 deletions src/core/chip-select/chip/__tests__/chip.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import { ChipSelectChip } from '../chip'
import { expect, test, vi } from 'vitest'
import { fireEvent, render, screen } from '@testing-library/react'

test('renders as checkbox element', () => {
render(
<ChipSelectChip size="small" value="foo">
Label
</ChipSelectChip>,
)
expect(screen.getByRole('checkbox')).toBeVisible()
})

test('checkbox is correctly labelled', () => {
render(
<ChipSelectChip size="small" value="foo">
Label
</ChipSelectChip>,
)
expect(screen.getByRole('checkbox', { name: 'Label' })).toBeVisible()
})

test('calls `onChange` when clicked', () => {
const handleChange = vi.fn()
render(
<ChipSelectChip onChange={handleChange} size="small" value="foo">
Label
</ChipSelectChip>,
)
fireEvent.click(screen.getByRole('checkbox'))
expect(handleChange).toHaveBeenCalled()
})

test('calls `onChange` when label is clicked', () => {
const handleChange = vi.fn()
render(
<ChipSelectChip onChange={handleChange} size="small" value="foo">
Label
</ChipSelectChip>,
)
fireEvent.click(screen.getByText('Label'))
expect(handleChange).toHaveBeenCalled()
})

test('checkbox has `type="checkbox"` attribute', () => {
render(
<ChipSelectChip size="small" value="foo">
Label
</ChipSelectChip>,
)
expect(screen.getByRole('checkbox')).toHaveAttribute('type', 'checkbox')
})

test('checkbox is unchecked by default', () => {
render(
<ChipSelectChip size="small" value="foo">
Label
</ChipSelectChip>,
)
expect(screen.getByRole('checkbox')).not.toBeChecked()
})

test('checkbox is checked when `checked` prop is true', () => {
render(
<ChipSelectChip checked onChange={vi.fn()} size="small" value="foo">
Label
</ChipSelectChip>,
)
expect(screen.getByRole('checkbox')).toBeChecked()
})

test('checkbox is disabled when `disabled` prop is true', () => {
render(
<ChipSelectChip disabled size="small" value="foo">
Label
</ChipSelectChip>,
)
expect(screen.getByRole('checkbox')).toBeDisabled()
})

test('disabled checkbox does not call `onChange`', () => {
const handleChange = vi.fn()
render(
<ChipSelectChip disabled onChange={handleChange} size="small" value="foo">
Label
</ChipSelectChip>,
)
fireEvent.click(screen.getByRole('checkbox'))
expect(handleChange).not.toHaveBeenCalled()
})

test('applies `aria-label` to the root element', () => {
const { container } = render(<ChipSelectChip aria-label="Test label" size="small" value="foo" />)
expect(container.firstElementChild).toHaveAttribute('aria-label', 'Test label')
})

test('applies `data-size` to the root element', () => {
const { container } = render(<ChipSelectChip aria-label="Test label" size="large" value="foo" />)
expect(container.firstElementChild).toHaveAttribute('data-size', 'large')
})

test('applies `data-overflow="truncate"` to label text when `overflow="truncate"` is provided', () => {
render(
<ChipSelectChip overflow="truncate" size="small" value="foo">
Label
</ChipSelectChip>,
)
expect(screen.getByText('Label')).toHaveAttribute('data-overflow', 'truncate')
})

test('displays the icon when provided', () => {
render(<ChipSelectChip icon="Fake icon" size="small" value="foo" />)
expect(screen.getByText('Fake icon')).toBeVisible()
})

test('icon is `aria-hidden`', () => {
render(<ChipSelectChip icon="Fake icon" size="small" value="foo" />)
expect(screen.getByText('Fake icon')).toHaveAttribute('aria-hidden', 'true')
})

test('forwards ref to the checkbox input', () => {
const ref = vi.fn()
render(
<ChipSelectChip ref={ref} size="small" value="foo">
Label
</ChipSelectChip>,
)
expect(ref).toHaveBeenCalledWith(expect.any(HTMLInputElement))
})

test('forwards additional attributes to the checkbox', () => {
render(
<ChipSelectChip data-testid="custom-chip" name="test-name" size="small" value="test-value">
Label
</ChipSelectChip>,
)
expect(screen.getByTestId('custom-chip')).toBe(screen.getByRole('checkbox'))
})

test('prevents other chips being selected when `isExclusive`', () => {
render(
<form>
<ChipSelectChip isExclusive name="test" size="small" value="foo">
Foo
</ChipSelectChip>
<ChipSelectChip isExclusive name="test" size="small" value="bar">
Bar
</ChipSelectChip>
</form>,
)
expect(screen.getByRole('checkbox', { name: 'Foo' })).not.toBeChecked()
expect(screen.getByRole('checkbox', { name: 'Bar' })).not.toBeChecked()

fireEvent.click(screen.getByRole('checkbox', { name: 'Foo' }))
expect(screen.getByRole('checkbox', { name: 'Foo' })).toBeChecked()
expect(screen.getByRole('checkbox', { name: 'Bar' })).not.toBeChecked()

fireEvent.click(screen.getByRole('checkbox', { name: 'Bar' }))
expect(screen.getByRole('checkbox', { name: 'Foo' })).not.toBeChecked()
expect(screen.getByRole('checkbox', { name: 'Bar' })).toBeChecked()
})

test('allows other chips to be selected when NOT `isExclusive`', () => {
render(
<form>
<ChipSelectChip name="test" size="small" value="foo">
Foo
</ChipSelectChip>
<ChipSelectChip name="test" size="small" value="bar">
Bar
</ChipSelectChip>
</form>,
)
expect(screen.getByRole('checkbox', { name: 'Foo' })).not.toBeChecked()
expect(screen.getByRole('checkbox', { name: 'Bar' })).not.toBeChecked()

fireEvent.click(screen.getByRole('checkbox', { name: 'Foo' }))
expect(screen.getByRole('checkbox', { name: 'Foo' })).toBeChecked()
expect(screen.getByRole('checkbox', { name: 'Bar' })).not.toBeChecked()

fireEvent.click(screen.getByRole('checkbox', { name: 'Bar' }))
expect(screen.getByRole('checkbox', { name: 'Foo' })).toBeChecked()
expect(screen.getByRole('checkbox', { name: 'Bar' })).toBeChecked()
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { maybeDeselectOtherOptions } from '../maybe-deselect-other-options'

test('deselects other checked options when newly selected option is exclusive', () => {
const option1 = createInput({ name: 'test-options', checked: true, exclusive: true })
const option2 = createInput({ name: 'test-options', checked: true })
const option3 = createInput({ name: 'test-options', checked: false })

const form = createFormWithInputs([option1, option2, option3])
mockRadioNodeList([option1, option2, option3], form)

maybeDeselectOtherOptions(option1)

expect(option1.checked).toBe(true) // Newly selected option remains checked
expect(option2.checked).toBe(false) // Other checked option gets unchecked
expect(option3.checked).toBe(false) // Already unchecked option remains unchecked
})

test('does not deselect other options when newly selected option is not exclusive', () => {
const option1 = createInput({ name: 'test-options', checked: true })
const option2 = createInput({ name: 'test-options', checked: true })

const form = createFormWithInputs([option1, option2])
mockRadioNodeList([option1, option2], form)

maybeDeselectOtherOptions(option1)

expect(option1.checked).toBe(true)
expect(option2.checked).toBe(true) // Should remain checked
})

test('does not deselect other options when exclusive is set to false', () => {
const option1 = createInput({ name: 'test-options', checked: true, exclusive: false })
const option2 = createInput({ name: 'test-options', checked: true })

const form = createFormWithInputs([option1, option2])
mockRadioNodeList([option1, option2], form)

maybeDeselectOtherOptions(option1)

expect(option1.checked).toBe(true)
expect(option2.checked).toBe(true) // Should remain checked
})

test('does not deselect when option has no associated form', () => {
const option1 = createInput({ name: 'test-options', checked: true, exclusive: true })
const option2 = createInput({ name: 'test-options', checked: true })

maybeDeselectOtherOptions(option1)

expect(option1.checked).toBe(true)
expect(option2.checked).toBe(true) // Should remain checked
})

test('does not deselect when option has no name', () => {
const option1 = createInput({ checked: true, exclusive: true })
const option2 = createInput({ checked: true })

createFormWithInputs([option1, option2])

maybeDeselectOtherOptions(option1)

expect(option1.checked).toBe(true)
expect(option2.checked).toBe(true) // Should remain checked
})

test('handles single option case gracefully', () => {
const option1 = createInput({ name: 'test-options', checked: true, exclusive: true })

const form = createFormWithInputs([option1])
vi.spyOn(form.elements, 'namedItem').mockReturnValue(option1)

maybeDeselectOtherOptions(option1)

expect(option1.checked).toBe(true) // Should remain checked
})

test('only unchecks options that are currently checked', () => {
const option1 = createInput({ name: 'test-options', checked: true, exclusive: true })
const option2 = createInput({ name: 'test-options', checked: true })
const option3 = createInput({ name: 'test-options', checked: false })

const form = createFormWithInputs([option1, option2, option3])
mockRadioNodeList([option1, option2, option3], form)

// Spy on setting checked property to verify behavior
const option2CheckedSetter = vi.fn()
const option3CheckedSetter = vi.fn()

Object.defineProperty(option2, 'checked', {
get: () => true,
set: option2CheckedSetter,
})

Object.defineProperty(option3, 'checked', {
get: () => false,
set: option3CheckedSetter,
})

maybeDeselectOtherOptions(option1)

expect(option2CheckedSetter).toHaveBeenCalledWith(false)
expect(option3CheckedSetter).not.toHaveBeenCalled() // Already unchecked, shouldn't be touched
})

interface CreateInputOptions {
name?: string
checked?: boolean
exclusive?: boolean | string
type?: string
}

function createInput(options: CreateInputOptions = {}): HTMLInputElement {
const input = document.createElement('input')
input.type = options.type || 'checkbox'

if (options.name) {
input.name = options.name
}

if (options.checked !== undefined) {
input.checked = options.checked
}

if (options.exclusive !== undefined) {
input.dataset.exclusive = String(options.exclusive)
}

return input
}

function createFormWithInputs(inputs: HTMLInputElement[]): HTMLFormElement {
const form = document.createElement('form')
inputs.forEach((input) => form.appendChild(input))
return form
}

function mockRadioNodeList(inputs: HTMLInputElement[], form: HTMLFormElement): void {
const mockRadioNodeList = inputs as any
mockRadioNodeList.forEach = Array.prototype.forEach
Object.setPrototypeOf(mockRadioNodeList, RadioNodeList.prototype)
vi.spyOn(form.elements, 'namedItem').mockReturnValue(mockRadioNodeList)
}
Loading
Loading