Skip to content

Commit

Permalink
test(dropdown): testing main dropdown features
Browse files Browse the repository at this point in the history
  • Loading branch information
Powerplex committed Dec 1, 2023
1 parent 586a285 commit a6efea4
Show file tree
Hide file tree
Showing 4 changed files with 262 additions and 26 deletions.
14 changes: 9 additions & 5 deletions packages/components/dropdown/src/Dropdown.doc.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,6 @@ import { Dropdown } from '@spark-ui/dropdown'

<Canvas of={stories.ControlledOpenState} />

### Custom item

<Canvas of={stories.CustomItem} />

### Disabled

TODO
Expand All @@ -103,20 +99,28 @@ TODO

TODO

## Trigger leading icon
### Trigger leading icon

TODO

### Multiple selection

<Canvas of={stories.MultipleSelection} />

### Multiple selection (controlled)

<Canvas of={stories.MultipleSelectionControlled} />

### Read only

TODO

## Advanced usage

### Custom item

<Canvas of={stories.CustomItem} />

### With form field label

<Canvas of={stories.FormFieldLabel} />
41 changes: 32 additions & 9 deletions packages/components/dropdown/src/Dropdown.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export default meta

export const Default: StoryFn = _args => {
return (
<div className="w-sz-480 pb-[300px]">
<div className="pb-[300px]">
<Dropdown>
<Dropdown.Trigger aria-label="Book">
<Dropdown.Value placeholder="Pick a book" />
Expand All @@ -40,7 +40,7 @@ export const Controlled: StoryFn = () => {
const [value, setValue] = useState('book-1')

return (
<div className="w-sz-480 pb-[300px]">
<div className="pb-[300px]">
<Dropdown value={value} onValueChange={setValue}>
<Dropdown.Trigger aria-label="Book">
<Dropdown.Value placeholder="Pick a book" />
Expand All @@ -62,7 +62,7 @@ export const Controlled: StoryFn = () => {
}

export const ControlledOpenState: StoryFn = () => {
const [open, setOpen] = useState(false)
const [open, setOpen] = useState(true)

return (
<div className="flex flex-col gap-lg">
Expand All @@ -75,7 +75,7 @@ export const ControlledOpenState: StoryFn = () => {
</Button>
</div>

<div className="w-sz-480 pb-[300px]">
<div className="pb-[300px]">
<Dropdown open={open} onOpenChange={setOpen}>
<Dropdown.Trigger aria-label="Book">
<Dropdown.Value placeholder="Pick a book" />
Expand All @@ -99,7 +99,7 @@ export const ControlledOpenState: StoryFn = () => {

export const CustomItem: StoryFn = _args => {
return (
<div className="w-sz-480 pb-[300px]">
<div className="pb-[300px]">
<Dropdown>
<Dropdown.Trigger aria-label="Book">
<Dropdown.Value placeholder="Pick a book" />
Expand Down Expand Up @@ -140,7 +140,7 @@ export const CustomItem: StoryFn = _args => {

export const DisabledItem: StoryFn = _args => {
return (
<div className="w-sz-480 pb-[300px]">
<div className="pb-[300px]">
<Dropdown>
<Dropdown.Trigger aria-label="Book">
<Dropdown.Value placeholder="Pick a book" />
Expand All @@ -165,7 +165,7 @@ export const DisabledItem: StoryFn = _args => {

export const Grouped: StoryFn = _args => {
return (
<div className="w-sz-480 pb-[300px]">
<div className="pb-[300px]">
<Dropdown>
<Dropdown.Trigger aria-label="Book">
<Dropdown.Value placeholder="Pick a book" />
Expand Down Expand Up @@ -196,7 +196,7 @@ export const Grouped: StoryFn = _args => {

export const FormFieldLabel: StoryFn = _args => {
return (
<div className="w-sz-480 pb-[300px]">
<div className="pb-[300px]">
<FormField>
<FormField.Label>Book</FormField.Label>
<Dropdown>
Expand All @@ -221,7 +221,30 @@ export const FormFieldLabel: StoryFn = _args => {

export const MultipleSelection: StoryFn = _args => {
return (
<div className="w-sz-480 pb-[300px]">
<div className="pb-[300px]">
<Dropdown multiple>
<Dropdown.Trigger aria-label="Book">
<Dropdown.Value placeholder="Pick a book" />
</Dropdown.Trigger>

<Dropdown.Popover>
<Dropdown.Items>
<Dropdown.Item value="book-1">To Kill a Mockingbird</Dropdown.Item>
<Dropdown.Item value="book-2">War and Peace</Dropdown.Item>
<Dropdown.Item value="book-3">The Idiot</Dropdown.Item>
<Dropdown.Item value="book-4">A Picture of Dorian Gray</Dropdown.Item>
<Dropdown.Item value="book-5">1984</Dropdown.Item>
<Dropdown.Item value="book-6">Pride and Prejudice</Dropdown.Item>
</Dropdown.Items>
</Dropdown.Popover>
</Dropdown>
</div>
)
}

export const MultipleSelectionControlled: StoryFn = _args => {
return (
<div className="pb-[300px]">
<Dropdown multiple>
<Dropdown.Trigger aria-label="Book">
<Dropdown.Value placeholder="Pick a book" />
Expand Down
232 changes: 220 additions & 12 deletions packages/components/dropdown/src/Dropdown.test.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,239 @@
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { useState } from 'react'
import { describe, expect, it } from 'vitest'

import { Dropdown } from '.'

const getTrigger = (accessibleName: string) => {
return screen.getByRole('combobox', { name: accessibleName })
}

const getListbox = (accessibleName: string) => {
return screen.getByRole('listbox', { name: accessibleName })
}

const getItem = (accessibleName: string) => {
return screen.getByRole('option', { name: accessibleName })
}

describe('Dropdown', () => {
describe('initial rendering', () => {
it('should render trigger and list of options', () => {
it('should render trigger and list of options', () => {
render(
<Dropdown>
<Dropdown.Trigger aria-label="Book">
<Dropdown.Value placeholder="Pick a book" />
</Dropdown.Trigger>
<Dropdown.Popover>
<Dropdown.Items>
<Dropdown.Item value="book-1">War and Peace</Dropdown.Item>
<Dropdown.Item value="book-2">1984</Dropdown.Item>
<Dropdown.Item value="book-3">Pride and Prejudice</Dropdown.Item>
</Dropdown.Items>
</Dropdown.Popover>
</Dropdown>
)

expect(getTrigger('Book')).toBeInTheDocument()

expect(getListbox('Book')).toBeInTheDocument()

expect(getItem('War and Peace')).toBeInTheDocument()
expect(getItem('1984')).toBeInTheDocument()
expect(getItem('Pride and Prejudice')).toBeInTheDocument()
})

describe('Popover behaviour', () => {
it('should open/close the popover when interacting with its trigger', async () => {
const user = userEvent.setup()

// Given a close dropdown (default state)
render(
<Dropdown>
<Dropdown.Trigger aria-label="Book">
<Dropdown.Value placeholder="Pick a book" />
</Dropdown.Trigger>
<Dropdown.Items aria-label="Job type">
<Dropdown.Item value="book-2">War and Peace</Dropdown.Item>
<Dropdown.Item value="book-5">1984</Dropdown.Item>
<Dropdown.Item value="book-6">Pride and Prejudice</Dropdown.Item>
</Dropdown.Items>
<Dropdown.Popover>
<Dropdown.Items>
<Dropdown.Item value="book-1">War and Peace</Dropdown.Item>
<Dropdown.Item value="book-2">1984</Dropdown.Item>
<Dropdown.Item value="book-3">Pride and Prejudice</Dropdown.Item>
</Dropdown.Items>
</Dropdown.Popover>
</Dropdown>
)

const trigger = getTrigger('Book')

expect(trigger).toHaveAttribute('aria-expanded', 'false')

// When the user interact with the trigger
await user.click(trigger)

// Then the dropdown has expanded
expect(trigger).toHaveAttribute('aria-expanded', 'true')

// When the user interact with the trigger while expanded
await user.click(trigger)

// Then the dropdown is closed again
expect(trigger).toHaveAttribute('aria-expanded', 'false')
})
})

describe('Dropdown.Value', () => {
it('should display placholder before selection, selected value after selection', async () => {
const user = userEvent.setup()

// Given a dropdown with no selected value yet
render(
<Dropdown>
<Dropdown.Trigger aria-label="Book">
<Dropdown.Value placeholder="Pick a book" />
</Dropdown.Trigger>
<Dropdown.Popover>
<Dropdown.Items>
<Dropdown.Item value="book-1">War and Peace</Dropdown.Item>
<Dropdown.Item value="book-2">1984</Dropdown.Item>
<Dropdown.Item value="book-3">Pride and Prejudice</Dropdown.Item>
</Dropdown.Items>
</Dropdown.Popover>
</Dropdown>
)

// Then placeholder should be displayed
expect(getTrigger('Book')).toHaveTextContent('Pick a book')

// When the user select an item
await user.click(getTrigger('Book'))
await user.click(getItem('Pride and Prejudice'))

// Then placeholder is replaced by the selected value
expect(getTrigger('Book')).toHaveTextContent('Pride and Prejudice')
})

it('should display custom value after selection', async () => {
const user = userEvent.setup()

// Given a dropdown with no selected value yet
render(
<Dropdown>
<Dropdown.Trigger aria-label="Book">
<Dropdown.Value placeholder="Pick a book">You have selected a book</Dropdown.Value>
</Dropdown.Trigger>
<Dropdown.Popover>
<Dropdown.Items>
<Dropdown.Item value="book-1">War and Peace</Dropdown.Item>
<Dropdown.Item value="book-2">1984</Dropdown.Item>
<Dropdown.Item value="book-3">Pride and Prejudice</Dropdown.Item>
</Dropdown.Items>
</Dropdown.Popover>
</Dropdown>
)

// Then placeholder should be displayed
expect(getTrigger('Book')).toHaveTextContent('Pick a book')

// When the user select an item
await user.click(getTrigger('Book'))
await user.click(getItem('Pride and Prejudice'))

// Then placeholder is replaced by a custom value
expect(getTrigger('Book')).toHaveTextContent('You have selected a book')
})
})

describe('default value', () => {
it('should render default selected option (single selection)', () => {
// Given a dropdown with a default selected value
render(
<Dropdown defaultValue="book-2">
<Dropdown.Trigger aria-label="Book">
<Dropdown.Value placeholder="Pick a book" />
</Dropdown.Trigger>
<Dropdown.Popover>
<Dropdown.Items>
<Dropdown.Item value="book-1">War and Peace</Dropdown.Item>
<Dropdown.Item value="book-2">1984</Dropdown.Item>
<Dropdown.Item value="book-3">Pride and Prejudice</Dropdown.Item>
</Dropdown.Items>
</Dropdown.Popover>
</Dropdown>
)

// Then the corresponding item is selected
expect(getItem('1984')).toHaveAttribute('aria-selected', 'true')
})

// it('should render default selected option (multiple selection)', () => {})
})

describe('controlled', () => {
it('should control value (single selection)', async () => {
const user = userEvent.setup()

// Given we control value by outside state and selected value
const ControlledImplementation = () => {
const [value, setValue] = useState('book-1')

return (
<Dropdown value={value} onValueChange={setValue}>
<Dropdown.Trigger aria-label="Book">
<Dropdown.Value placeholder="Pick a book" />
</Dropdown.Trigger>
<Dropdown.Popover>
<Dropdown.Items>
<Dropdown.Item value="book-1">War and Peace</Dropdown.Item>
<Dropdown.Item value="book-2">1984</Dropdown.Item>
<Dropdown.Item value="book-3">Pride and Prejudice</Dropdown.Item>
</Dropdown.Items>
</Dropdown.Popover>
</Dropdown>
)
}

render(<ControlledImplementation />)

expect(getItem('War and Peace')).toHaveAttribute('aria-selected', 'true')

expect(getTrigger('Book')).toHaveTextContent('War and Peace')

// when the user select another item
await user.click(getTrigger('Book'))
await user.click(getItem('Pride and Prejudice'))

// Then the selected value has been updated
expect(getTrigger('Book')).toHaveTextContent('Pride and Prejudice')
})

// it('should control value (multiple selection)', async () => {})

it('should remain forced opened', async () => {
const user = userEvent.setup()

// Given a dropdown that should remain opened
render(
<Dropdown open>
<Dropdown.Trigger aria-label="Book">
<Dropdown.Value placeholder="Pick a book" />
</Dropdown.Trigger>
<Dropdown.Popover>
<Dropdown.Items>
<Dropdown.Item value="book-1">War and Peace</Dropdown.Item>
<Dropdown.Item value="book-2">1984</Dropdown.Item>
<Dropdown.Item value="book-3">Pride and Prejudice</Dropdown.Item>
</Dropdown.Items>
</Dropdown.Popover>
</Dropdown>
)

expect(screen.getByRole('combobox', { name: 'Book' })).toBeInTheDocument()
expect(getTrigger('Book')).toHaveAttribute('aria-expanded', 'true')

expect(screen.getByRole('listbox', { name: 'Book' })).toBeInTheDocument()
// When the user interacts with the trigger
await user.click(getTrigger('Book'))

expect(screen.getByRole('option', { name: 'War and Peace' })).toBeInTheDocument()
expect(screen.getByRole('option', { name: '1984' })).toBeInTheDocument()
expect(screen.getByRole('option', { name: 'Pride and Prejudice' })).toBeInTheDocument()
// Then the dropdown remains opened
expect(getTrigger('Book')).toHaveAttribute('aria-expanded', 'true')
})
})
})
Loading

0 comments on commit a6efea4

Please sign in to comment.