Skip to content

Commit

Permalink
fix: Let wave app programmatically change value and choices for combo…
Browse files Browse the repository at this point in the history
…box #1501 (#1538)
  • Loading branch information
aalencar authored Aug 8, 2022
1 parent 3d1b534 commit 5eb050e
Show file tree
Hide file tree
Showing 3 changed files with 339 additions and 104 deletions.
2 changes: 1 addition & 1 deletion py/examples/combobox.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ async def serve(q: Q):
choices=combobox_choices, disabled=True),
ui.combobox(name='combobox_error', label='Enter or choose a color', placeholder='Color...', value='Blue',
choices=combobox_choices, error='This combobox has an error!'),
ui.combobox(name='combobox_multivalued', label='Enter or choose a color (multi select)', placeholder='Color...', values=['Black', 'Magenta'],
ui.combobox(name='combobox_multivalued', label='Enter or choose a color (multi select)', placeholder='Color...', values=['Blue', 'Magenta'],
choices=combobox_choices),
ui.button(name='show_inputs', label='Submit', primary=True),
])
Expand Down
287 changes: 232 additions & 55 deletions ui/src/combobox.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { Combobox, XCombobox } from './combobox'
import { wave } from './ui'

const name = 'combobox'
const comboboxProps: Combobox = { name, choices: ['Choice1', 'Choice2', 'Choice3'] }
const comboboxProps: Combobox = { name, choices: ['A', 'B', 'C'] }
describe('Combobox.tsx', () => {
beforeEach(() => { wave.args[name] = null })

Expand All @@ -28,71 +28,248 @@ describe('Combobox.tsx', () => {
expect(queryByTestId(name)).toBeInTheDocument()
})

it('Sets args - init - value not specified', () => {
render(<XCombobox model={comboboxProps} />)
expect(wave.args[name]).toBeNull()
})
it('Sets args - init - value specified', () => {
render(<XCombobox model={{ ...comboboxProps, value: 'Test' }} />)
expect(wave.args[name]).toBe('Test')
})
describe('Single Select', () => {
it('Displays new typed option', () => {
const { getByRole } = render(<XCombobox model={{ ...comboboxProps, value: 'A' }} />)
expect(wave.args[name]).toEqual('A')
userEvent.type(getByRole('combobox'), '{backspace}D{enter}')
expect(wave.args[name]).toEqual('D')
})

it('Sets args - selection', () => {
const { getByRole, getByText } = render(<XCombobox model={{ ...comboboxProps }} />)
fireEvent.click(getByRole('presentation', { hidden: true }))
fireEvent.click(getByText('Choice1'))
it('Displays new option when clicking away after typing', () => {
const { getByRole } = render(<XCombobox model={{ ...comboboxProps, value: 'A' }} />)
const combobox = getByRole('combobox')

expect(wave.args[name]).toBe('Choice1')
})
userEvent.type(combobox, '{backspace}D')
fireEvent.blur(combobox)
expect(combobox).toHaveValue('D')
})

it('Sets args - multiple selection', () => {
const { getByRole, getByText } = render(<XCombobox model={{ ...comboboxProps, values: [] }} />)
fireEvent.click(getByRole('presentation', { hidden: true }))
fireEvent.click(getByText('Choice1'))
fireEvent.click(getByText('Choice2'))
it('Adds new typed option only once to options list', () => {
const { getAllByRole, getAllByTitle, getByRole } = render(<XCombobox model={{ ...comboboxProps, value: 'A' }} />)
userEvent.type(getByRole('combobox'), '{backspace}D{enter}')
fireEvent.click(getByRole('presentation', { hidden: true }))
expect(getAllByRole('option')).toHaveLength(4)
expect(getAllByTitle('D')).toHaveLength(1)
})

expect(wave.args[name]).toEqual(['Choice1', 'Choice2'])
})
describe('Wave args', () => {
it('Sets args to null when initial value is not specified', () => {
render(<XCombobox model={comboboxProps} />)
expect(wave.args[name]).toBeNull()
})
it('Sets args to initial value', () => {
render(<XCombobox model={{ ...comboboxProps, value: 'D' }} />)
expect(wave.args[name]).toBe('D')
})

it('Sets args to manually selected option', () => {
const { getByRole, getByTitle } = render(<XCombobox model={{ ...comboboxProps }} />)
fireEvent.click(getByRole('presentation', { hidden: true }))
fireEvent.click(getByTitle('A'))
expect(wave.args[name]).toBe('A')

it('Types new option', () => {
const initialValues = ['Choice1']
const { getByRole } = render(<XCombobox model={{ ...comboboxProps, values: initialValues }} />)
fireEvent.click(getByRole('presentation', { hidden: true }))
fireEvent.click(getByTitle('B'))
expect(wave.args[name]).toBe('B')
})

expect(wave.args[name]).toEqual(initialValues)
userEvent.type(getByRole('combobox'), 'Choice4{Enter}')
expect(wave.args[name]).toEqual(['Choice1', 'Choice4'])
})
it('Calls sync when trigger is on', () => {
const pushMock = jest.fn()
wave.push = pushMock
const { getByRole, getByText } = render(<XCombobox model={{ ...comboboxProps, trigger: true }} />)

fireEvent.click(getByRole('presentation', { hidden: true }))
fireEvent.click(getByText('A'))

expect(pushMock).toHaveBeenCalled()
})
})

it('Unselects every option and types a new one', () => {
const initialValues = ['Choice1', 'Choice2']
const { getByText, getByRole } = render(<XCombobox model={{ ...comboboxProps, values: initialValues }} />)

expect(wave.args[name]).toEqual(initialValues)
fireEvent.click(getByRole('presentation', { hidden: true }))
fireEvent.click(getByText('Choice1'))
fireEvent.click(getByText('Choice2'))
expect(wave.args[name]).toEqual([])
userEvent.type(getByRole('combobox'), 'Choice4{Enter}')
expect(wave.args[name]).toEqual(['Choice4'])
})
describe('Prop changes - update values dynamically from Wave app', () => {
it('Displays new options in options list when "choices" prop is updated', () => {
const { getByRole, getAllByRole, rerender } = render(<XCombobox model={{ ...comboboxProps, value: 'A' }} />)
fireEvent.click(getByRole('presentation', { hidden: true }))
expect(getAllByRole('option')).toHaveLength(3)

rerender(<XCombobox model={{ ...comboboxProps, choices: ['A', 'B'] }} />)
expect(getAllByRole('option')).toHaveLength(2)
})

it('Calls sync when trigger is on', () => {
const pushMock = jest.fn()
wave.push = pushMock
const { getByRole, getByText } = render(<XCombobox model={{ ...comboboxProps, trigger: true }} />)
it('Updates combobox value when "value" prop changes', () => {
const { getByRole, getByText, rerender } = render(<XCombobox model={{ ...comboboxProps, value: 'A' }} />)
expect(getByRole('combobox')).toHaveValue('A')
expect(wave.args[name]).toEqual('A')

rerender(<XCombobox model={{ ...comboboxProps, value: 'B' }} />)
expect(getByRole('combobox')).toHaveValue('B')
expect(wave.args[name]).toEqual('B')

fireEvent.click(getByRole('presentation', { hidden: true }))
fireEvent.click(getByText('C'))
expect(getByRole('combobox')).toHaveValue('C')
expect(wave.args[name]).toEqual('C')

rerender(<XCombobox model={{ ...comboboxProps, value: 'B' }} />)
expect(getByRole('combobox')).toHaveValue('B')
expect(wave.args[name]).toEqual('B')
})

it('Updates "choices" prop and "value" prop to value different than the initial one', () => {
const { getByRole, rerender } = render(<XCombobox model={{ ...comboboxProps, value: 'A' }} />)
expect(wave.args[name]).toEqual('A')
expect(getByRole('combobox')).toHaveValue('A')

rerender(<XCombobox model={{ ...comboboxProps, choices: ['A', 'B'], value: 'B' }} />)
expect(getByRole('combobox')).toHaveValue('B')
})

it('Types new option and then updates combobox value when "value" prop changes', () => {
const { getByRole, getByText, rerender } = render(<XCombobox model={{ ...comboboxProps, value: 'A' }} />)
expect(getByRole('combobox')).toHaveValue('A')
expect(wave.args[name]).toEqual('A')

fireEvent.click(getByRole('presentation', { hidden: true }))
fireEvent.click(getByText('B'))
fireEvent.blur(getByRole('presentation', { hidden: true }))
userEvent.type(getByRole('combobox'), 'B{Enter}')
expect(getByRole('combobox')).toHaveValue('BB')
expect(wave.args[name]).toEqual('BB')

rerender(<XCombobox model={{ ...comboboxProps, value: 'C' }} />)
expect(getByRole('combobox')).toHaveValue('C')
expect(wave.args[name]).toEqual('C')
})

it('Adds initial value to options if it\'s not included in "choices" prop', () => {
const { getByTitle, getAllByRole, getByRole } = render(<XCombobox model={{ ...comboboxProps, value: 'Z' }} />)
expect(wave.args[name]).toEqual('Z')
expect(getByRole('combobox')).toHaveValue('Z')
fireEvent.click(getByRole('presentation', { hidden: true }))
expect(getAllByRole('option')).toHaveLength(4)
expect(getByTitle('Z')).toBeDefined()
})

fireEvent.click(getByRole('presentation', { hidden: true }))
fireEvent.click(getByText('Choice1'))
it('Adds value to choices when both are updated and value was included in previous choices but not in the new choices', () => {
const { getByRole, getAllByRole, getByTitle, rerender } = render(<XCombobox model={{ ...comboboxProps, value: 'A' }} />)
expect(getByRole('combobox')).toHaveValue('A')

rerender(<XCombobox model={{ ...comboboxProps, value: 'C', choices: ['A', 'B'] }} />)
expect(getByRole('combobox')).toHaveValue('C')
fireEvent.click(getByRole('presentation', { hidden: true }))
expect(getAllByRole('option')).toHaveLength(3)
expect(getByTitle('C')).toBeDefined()
})

expect(pushMock).toHaveBeenCalled()
it('Display same value if choices change and value is included in choices', () => {
const { getByRole, rerender, } = render(<XCombobox model={{ ...comboboxProps, value: 'A' }} />, { })
expect(getByRole('combobox')).toHaveValue('A')
rerender(<XCombobox model={{ ...comboboxProps, choices: ['A', 'B'], value: 'A' }} />)
expect(getByRole('combobox')).toHaveValue('A')
})
})
})

it('Selects and unselects a user typed option', () => {
const { getByRole, getByText } = render(<XCombobox model={{ ...comboboxProps, values: [] }} />)
userEvent.type(getByRole('combobox'), 'Choice4{Enter}')
expect(wave.args[name]).toEqual(['Choice4'])
fireEvent.click(getByRole('presentation', { hidden: true }))
fireEvent.click(getByText('Choice4'))
expect(wave.args[name]).toEqual([])
describe('Multi Select', () => {
it('Selects and unselects a user typed option', () => {
const { getByRole, getByText } = render(<XCombobox model={{ ...comboboxProps, values: [] }} />)
userEvent.type(getByRole('combobox'), 'D{Enter}')
expect(wave.args[name]).toEqual(['D'])
fireEvent.click(getByRole('presentation', { hidden: true }))
fireEvent.click(getByText('D'))
expect(wave.args[name]).toEqual([])
})

it('Displays new typed option', () => {
const { getByRole } = render(<XCombobox model={{ ...comboboxProps, values: ['A'] }} />)
expect(wave.args[name]).toEqual(['A'])

userEvent.type(getByRole('combobox'), 'D{enter}')
expect(wave.args[name]).toEqual(['A', 'D'])
})

it('Unselects every option and types a new one', () => {
const { getByText, getByRole } = render(<XCombobox model={{ ...comboboxProps, values: ['A', 'B'] }} />)
expect(wave.args[name]).toEqual(['A', 'B'])

fireEvent.click(getByRole('presentation', { hidden: true }))
fireEvent.click(getByText('A'))
fireEvent.click(getByText('B'))
expect(wave.args[name]).toEqual([])

userEvent.type(getByRole('combobox'), 'D{Enter}')
expect(wave.args[name]).toEqual(['D'])
})

describe('Wave args', () => {
it('Sets args to null when "values" is empty', () => {
render(<XCombobox model={{...comboboxProps, values: []}} />)
expect(wave.args[name]).toBeNull()
})

it('Sets args to initial values', () => {
render(<XCombobox model={{ ...comboboxProps, values: ['A', 'B'] }} />)
expect(wave.args[name]).toEqual(['A', 'B'])
})

it('Sets args to manually selected options', () => {
const { getByRole, getByText } = render(<XCombobox model={{ ...comboboxProps, values: [] }} />)
fireEvent.click(getByRole('presentation', { hidden: true }))
fireEvent.click(getByText('A'))
fireEvent.click(getByText('B'))

expect(wave.args[name]).toEqual(['A', 'B'])
})

it('Calls sync when trigger is on', () => {
const pushMock = jest.fn()
wave.push = pushMock
const { getByRole, getByText } = render(<XCombobox model={{ ...comboboxProps, values: [], trigger: true }} />)

fireEvent.click(getByRole('presentation', { hidden: true }))
fireEvent.click(getByText('A'))
fireEvent.click(getByText('B'))

expect(pushMock).toHaveBeenCalled()
})
})

describe('Prop changes - update values dynamically from Wave app', () => {
it('Types new option and then updates combobox value when "values" prop changes', () => {
const { getByRole, getByText, rerender } = render(<XCombobox model={{ ...comboboxProps, values: ['A', 'B'] }} />)
expect(getByRole('combobox')).toHaveValue('A, B')

rerender(<XCombobox model={{ ...comboboxProps, values: ['C'] }} />)
expect(getByRole('combobox')).toHaveValue('C')

fireEvent.click(getByRole('presentation', { hidden: true }))
fireEvent.click(getByText('B'))
fireEvent.blur(getByRole('presentation', { hidden: true }))
expect(getByRole('combobox')).toHaveValue('B, C')

rerender(<XCombobox model={{ ...comboboxProps, values: ['A', 'B'] }} />)
expect(getByRole('combobox')).toHaveValue('A, B')
})

it('Displays new options in options list when "choices" prop is updated', () => {
const { getByRole, getAllByRole, rerender } = render(<XCombobox model={{ ...comboboxProps, values: ['A'] }} />)
fireEvent.click(getByRole('presentation', { hidden: true }))
expect(getAllByRole('option')).toHaveLength(3)

rerender(<XCombobox model={{ ...comboboxProps, choices: ['A', 'B'] }} />)
fireEvent.click(getByRole('presentation', { hidden: true }))
expect(getAllByRole('option')).toHaveLength(2)
})

it('Adds initial values to options if they are not already included in options ', () => {
const { getByTitle, getAllByRole, getByRole } = render(<XCombobox model={{ ...comboboxProps, value: 'Z' }} />)
expect(wave.args[name]).toEqual('Z')
expect(getByRole('combobox')).toHaveValue('Z')
fireEvent.click(getByRole('presentation', { hidden: true }))
expect(getAllByRole('option')).toHaveLength(4)
expect(getByTitle('Z')).toBeDefined()
})
})
})
})
Loading

0 comments on commit 5eb050e

Please sign in to comment.