overleaf/services/web/test/frontend/components/shared/select.spec.tsx
Mathias Jakobsen 45274d9dff Merge pull request #16380 from overleaf/mj-select-keyboard
[web] Allow keyboard interactions with custom select component

GitOrigin-RevId: 81adea8e456bd6ce2483dfa17a352c24c36e5768
2024-01-08 09:04:30 +00:00

278 lines
8.3 KiB
TypeScript

import { useCallback, FormEvent } from 'react'
import { Button, Form, FormControl } from 'react-bootstrap'
import {
Select,
SelectProps,
} from '../../../../frontend/js/shared/components/select'
const testData = [1, 2, 3].map(index => ({
key: index,
value: `Demo item ${index}`,
sub: `Subtitle ${index}`,
}))
type RenderProps = Partial<SelectProps<typeof testData[number]>> & {
onSubmit?: (formData: object) => void
}
function render(props: RenderProps) {
const submitHandler = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault()
if (props.onSubmit) {
const formData = new FormData(event.target as HTMLFormElement)
// a plain object is more convenient to work later with assertions
props.onSubmit(Object.fromEntries(formData.entries()))
}
}
cy.mount(
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: '100vh',
}}
>
<form onSubmit={submitHandler}>
<Select
items={testData}
itemToString={x => String(x?.value)}
label={props.label}
name="select_control"
defaultText={props.defaultText}
defaultItem={props.defaultItem}
itemToSubtitle={props.itemToSubtitle}
itemToKey={x => String(x.key)}
onSelectedItemChanged={props.onSelectedItemChanged}
disabled={props.disabled}
optionalLabel={props.optionalLabel}
loading={props.loading}
/>
<button type="submit">submit</button>
</form>
</div>
)
}
describe('<Select />', function () {
describe('initial rendering', function () {
it('renders default text', function () {
render({ defaultText: 'Choose an item' })
cy.findByTestId('spinner').should('not.exist')
cy.findByText('Choose an item')
})
it('renders default item', function () {
render({ defaultItem: testData[2] })
cy.findByText('Demo item 3')
})
it('default item takes precedence over default text', function () {
render({ defaultText: 'Choose an item', defaultItem: testData[2] })
cy.findByText('Demo item 3')
})
it('renders label', function () {
render({
defaultText: 'Choose an item',
label: 'test label',
optionalLabel: false,
})
cy.findByText('test label')
cy.findByText('(Optional)').should('not.exist')
})
it('renders optional label', function () {
render({
defaultText: 'Choose an item',
label: 'test label',
optionalLabel: true,
})
cy.findByText('test label')
cy.findByText('(Optional)')
})
it('renders a spinner while loading when there is a label', function () {
render({
defaultText: 'Choose an item',
label: 'test label',
loading: true,
})
cy.findByTestId('spinner')
})
it('does not render a spinner while loading if there is no label', function () {
render({
defaultText: 'Choose an item',
loading: true,
})
cy.findByTestId('spinner').should('not.exist')
})
})
describe('items rendering', function () {
it('renders all items', function () {
render({ defaultText: 'Choose an item' })
cy.findByText('Choose an item').click()
cy.findByText('Demo item 1')
cy.findByText('Demo item 2')
cy.findByText('Demo item 3')
})
it('renders subtitles', function () {
render({
defaultText: 'Choose an item',
itemToSubtitle: x => String(x?.sub),
})
cy.findByText('Choose an item').click()
cy.findByText('Subtitle 1')
cy.findByText('Subtitle 2')
cy.findByText('Subtitle 3')
})
})
describe('item selection', function () {
it('cannot select an item when disabled', function () {
render({ defaultText: 'Choose an item', disabled: true })
cy.findByText('Choose an item').click()
cy.findByText('Demo item 1').should('not.exist')
cy.findByText('Demo item 2').should('not.exist')
cy.findByText('Demo item 3').should('not.exist')
cy.findByText('Choose an item')
})
it('renders only the selected item after selection', function () {
render({ defaultText: 'Choose an item' })
cy.findByText('Choose an item').click()
cy.findByText('Demo item 1')
cy.findByText('Demo item 2')
cy.findByText('Demo item 3').click()
cy.findByText('Choose an item').should('not.exist')
cy.findByText('Demo item 1').should('not.exist')
cy.findByText('Demo item 2').should('not.exist')
cy.findByText('Demo item 3')
})
it('invokes callback after selection', function () {
const selectionHandler = cy.stub().as('selectionHandler')
render({
defaultText: 'Choose an item',
onSelectedItemChanged: selectionHandler,
})
cy.findByText('Choose an item').click()
cy.findByText('Demo item 2').click()
cy.get('@selectionHandler').should(
'have.been.calledOnceWith',
testData[1]
)
})
})
describe('when the form is submitted', function () {
it('populates FormData with the default selected item', function () {
const submitHandler = cy.stub().as('submitHandler')
render({ defaultItem: testData[1], onSubmit: submitHandler })
cy.findByText('submit').click()
cy.get('@submitHandler').should('have.been.calledOnceWith', {
select_control: 'Demo item 2',
})
})
it('populates FormData with the selected item', function () {
const submitHandler = cy.stub().as('submitHandler')
render({ defaultItem: testData[1], onSubmit: submitHandler })
cy.findByText('Demo item 2').click() // open dropdown
cy.findByText('Demo item 3').click() // choose a different item
cy.findByText('submit').click()
cy.get('@submitHandler').should('have.been.calledOnceWith', {
select_control: 'Demo item 3',
})
})
it('does not populate FormData when no item is selected', function () {
const submitHandler = cy.stub().as('submitHandler')
render({ defaultText: 'Choose an item', onSubmit: submitHandler })
cy.findByText('submit').click()
cy.get('@submitHandler').should('have.been.calledOnceWith', {})
})
})
describe('with react-bootstrap forms', function () {
type FormWithSelectProps = {
onSubmit: (formData: object) => void
}
const FormWithSelect = ({ onSubmit }: FormWithSelectProps) => {
const selectComponent = useCallback(
() => (
<Select
name="select_control"
items={testData}
defaultItem={testData[0]}
itemToString={x => String(x?.value)}
itemToKey={x => String(x.key)}
/>
),
[]
)
function handleSubmit(event: FormEvent<Form>) {
event.preventDefault()
const formData = new FormData(event.target as HTMLFormElement)
// a plain object is more convenient to work later with assertions
onSubmit(Object.fromEntries(formData.entries()))
}
return (
<Form onSubmit={handleSubmit}>
<FormControl componentClass={selectComponent} />
<Button type="submit">submit</Button>
</Form>
)
}
it('populates FormData with the selected item when the form is submitted', function () {
const submitHandler = cy.stub().as('submitHandler')
cy.mount(
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: '100vh',
}}
>
<FormWithSelect onSubmit={submitHandler} />
</div>
)
cy.findByText('Demo item 1').click() // open dropdown
cy.findByText('Demo item 3').click() // choose a different item
cy.findByText('submit').click()
cy.get('@submitHandler').should('have.been.calledOnceWith', {
select_control: 'Demo item 3',
})
})
})
describe('keyboard navigation', function () {
it('can select an item using the keyboard', function () {
render({ defaultText: 'Choose an item' })
cy.findByText('Choose an item').type('{Enter}{downArrow}{Enter}')
cy.findByText('Demo item 1').should('exist')
cy.findByText('Demo item 2').should('not.exist')
})
})
})