Skip to content

Commit

Permalink
feat: allow continual typing of search term during api request
Browse files Browse the repository at this point in the history
  • Loading branch information
dmijatovic committed Sep 24, 2022
1 parent 82b9282 commit 152a7e6
Show file tree
Hide file tree
Showing 4 changed files with 297 additions and 171 deletions.
6 changes: 3 additions & 3 deletions frontend/components/form/AsyncAutocompleteSC.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ type AsyncAutocompleteProps<T> = {
) => ReactNode
config: AsyncAutocompleteConfig,
defaultValue?: AutocompleteOption<T>
onClear?: ()=>void
onClear?: () => void
}


Expand Down Expand Up @@ -138,12 +138,12 @@ export default function AsyncAutocompleteSC<T>({status, options, config,
onSearch(newInputValue)
setProcessing(newInputValue)
// debugger
} else if (loading === false &&
reason !== 'reset') {
} else if (reason !== 'reset') {
// reset reason occures when option is selected from the list
// because search text is usually not identical to selected item
// we ignore onInputChange event when reason==='reset'
setInputValue(newInputValue)

// we start new search if processing
// is not empty we should reset it??
if (processing !== '') {
Expand Down
286 changes: 162 additions & 124 deletions frontend/components/mention/FindMention.test.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,30 @@

import {render, screen, fireEvent, waitFor, waitForElementToBeRemoved, act} from '@testing-library/react'
import {render, screen, fireEvent, waitFor, act, waitForElementToBeRemoved} from '@testing-library/react'
import {MentionItemProps, MentionTypeKeys} from '~/types/Mention'
import FindMention from './FindMention'
import FindMention, {FindMentionProps} from './FindMention'

// default is non-resolved promise - for first test
const mockSearchFn = jest.fn((props) => new Promise<MentionItemProps[]>((res, rej) => {}))
const mockAdd = jest.fn()
const mockCreate = jest.fn()
// mock mention
const mockMentionItem = {
id: '1',
doi: 'doi-test',
url: 'url-test',
title: 'test-title',
authors: 'test-authors',
publisher: 'test-publisher',
publication_year: 2020,
page: null,
// url to external image
image_url: null,
// is_featured?: boolean
mention_type: 'book' as MentionTypeKeys,
source: 'crossref'
}

const props = {
let props:FindMentionProps = {
config: {
freeSolo: true,
minLength: 3,
Expand All @@ -21,11 +37,10 @@ const props = {
onCreate: mockCreate
}

beforeAll(() => {
jest.useFakeTimers()
})
jest.useFakeTimers()

it('has cancel button when freeSolo', async() => {
it('has working cancel button when freeSolo', async () => {
props.config.freeSolo=true
const searchFor = 'test string'
// render component
render(<FindMention {...props} />)
Expand All @@ -36,38 +51,36 @@ it('has cancel button when freeSolo', async() => {
// need to advance all timers for debounce etc...
// Note! it needs to be wrapped in act
act(() => {
jest.runAllTimers()
jest.runOnlyPendingTimers()
})

const cancelBtn = screen.getByRole('button', {hidden:true})
expect(cancelBtn).toBeInTheDocument()
expect(cancelBtn.getAttribute('title')).toEqual('Cancel')

// press the button
fireEvent.click(cancelBtn)
// shows loader. NOTE! we do not return promise by default
const loader = screen.getByTestId('circular-loader')
expect(loader).toBeInTheDocument()

// press cancel button
fireEvent.click(cancelBtn)
// confim that loader is gone
expect(loader).not.toBeInTheDocument()
})

it('shows custom notFound message when freeSolo AND onCreate NOT provided', async() => {
// prepare
jest.useFakeTimers()
// custom props
const props = {
config: {
freeSolo: true,
minLength: 3,
label: 'Test label',
help: 'Test help',
reset: true,
noOptions: {
notFound: 'Tested not found message',
empty: 'Not tested',
minLength: 'Not tested'
}
},
searchFn: mockSearchFn,
onAdd: mockAdd
it('shows custom notFound message when freeSolo AND onCreate NOT provided', async () => {
const notFound = 'Tested not found message'
props.config = {
...props.config,
freeSolo: true,
noOptions: {
notFound,
empty: 'Not tested',
minLength: 'Not tested'
}
}
props.onCreate = undefined

// resolve with no options
mockSearchFn.mockResolvedValueOnce([])

Expand All @@ -81,114 +94,139 @@ it('shows custom notFound message when freeSolo AND onCreate NOT provided', asyn

// need to advance all timers for debounce etc...
// Note! it needs to be wrapped in act
act(() => {
jest.runAllTimers()
await act(() => {
jest.runOnlyPendingTimers()
})

// then wait for loader to be removed
await waitForElementToBeRemoved(() => screen.getByTestId('circular-loader'))

// wait for listbox and notFound message
await waitFor(() => {
const options = screen.getByRole('listbox')
expect(options).toBeInTheDocument()
const notFound = screen.getByText(props.config.noOptions.notFound)
expect(notFound).toBeInTheDocument()
const notFoundMsg = screen.getByText(notFound)
expect(notFoundMsg).toBeInTheDocument()
})
})

describe('reset functionality', () => {
// custom props
const props = {
config: {
freeSolo: true,
minLength: 3,
label: 'Test label',
help: 'Test help',
reset: true,
noOptions: {
notFound: 'Tested not found message',
empty: 'Not tested',
minLength: 'Not tested'
}
},
searchFn: mockSearchFn,
onAdd: mockAdd
}
const mockMentionItem = {
id: '1',
doi: 'doi-test',
url: 'url-test',
title: 'test-title',
authors: 'test-authors',
publisher: 'test-publisher',
publication_year: 2020,
page: null,
// url to external image
image_url: null,
// is_featured?: boolean
mention_type: 'book' as MentionTypeKeys,
source: 'crossref'
}
it('does not show Add option when onCreate=undefined', async () => {
// resolve with no options
mockSearchFn.mockResolvedValueOnce([mockMentionItem,mockMentionItem])
// SET RESET TO FALSE
props.config.reset = false
// remove onCreate fn
props.onCreate = undefined
// render component
render(<FindMention {...props} />)
// input test value
const searchInput = screen.getByRole('combobox')
const searchFor = 'test'
fireEvent.change(searchInput, {target: {value: searchFor}})

// need to advance all timers for debounce etc...
// Note! it needs to be wrapped in act
await act(() => {
jest.runOnlyPendingTimers()
})

// select only option in dropdown
const options = screen.getAllByRole('option')
expect(options.length).toEqual(2)
// click first options
fireEvent.click(options[0])
expect(mockAdd).toBeCalledTimes(1)
expect(mockAdd).toBeCalledWith(mockMentionItem)
})

// NOT ALLOWED WITH MENTIONS but we test
it('shows Add option when onCreate defined', async () => {
// resolve with no options
mockSearchFn.mockResolvedValueOnce([mockMentionItem,mockMentionItem])
// SET RESET TO FALSE
props.config.reset = false
// remove onCreate fn
props.onCreate = mockCreate
// render component
render(<FindMention {...props} />)
// input test value
const searchInput = screen.getByRole('combobox')
const searchFor = 'test'
fireEvent.change(searchInput, {target: {value: searchFor}})

beforeEach(() => {
// prepare
jest.useFakeTimers()
// resolve with no options
mockSearchFn.mockResolvedValueOnce([mockMentionItem,mockMentionItem])
// need to advance all timers for debounce etc...
// Note! it needs to be wrapped in act
await act(() => {
jest.runOnlyPendingTimers()
})

it('removes input after selection when reset=true', async () => {
// render component
render(<FindMention {...props} />)
// input test value
const searchInput = screen.getByRole('combobox')
const searchFor = 'test'
fireEvent.change(searchInput, {target: {value: searchFor}})
// need to advance all timers for debounce etc...
// Note! it needs to be wrapped in act
act(() => {
jest.runAllTimers()
})

// then wait for loader to be removed
await waitForElementToBeRemoved(() => screen.getByTestId('circular-loader'))
const options = screen.getAllByRole('option')
expect(options.length).toEqual(2)

// select option
fireEvent.click(options[0])
// exper input to be reset
expect(searchInput).toHaveValue('')
// select only options in dropdown
const options = screen.getAllByRole('option')
expect(options.length).toEqual(3)

// find Add est
const add = screen.getByText(`Add "${searchFor}"`)
expect(add).toBeInTheDocument()

// select first option
const firstOption = options[0]
fireEvent.click(firstOption)
expect(mockCreate).toBeCalledTimes(1)
expect(mockCreate).toBeCalledWith(searchFor)
})

it('leaves input after selection when reset=false', async () => {
// resolve with no options
mockSearchFn.mockResolvedValueOnce([mockMentionItem,mockMentionItem])
// SET RESET TO FALSE
props.config.reset = false
// remove onCreate fn
props.onCreate = undefined
// render component
render(<FindMention {...props} />)
// input test value
const searchInput = screen.getByRole('combobox')
const searchFor = 'test'
fireEvent.change(searchInput, {target: {value: searchFor}})

// need to advance all timers for debounce etc...
// Note! it needs to be wrapped in act
await act(() => {
jest.runOnlyPendingTimers()
})

it('leaves input after selection when reset=false', async () => {
// SET RESET TO FALSE
props.config.reset=false
// render component
render(<FindMention {...props} />)
// input test value
const searchInput = screen.getByRole('combobox')
const searchFor = 'test'
fireEvent.change(searchInput, {target: {value: searchFor}})

// need to advance all timers for debounce etc...
// Note! it needs to be wrapped in act
act(() => {
jest.runAllTimers()
})

// then wait for loader to be removed
await waitForElementToBeRemoved(() => screen.getByTestId('circular-loader'))

// select only option in dropdown
const options = screen.getAllByRole('option')
expect(options.length).toEqual(2)

// click on the option
fireEvent.click(options[0])

// expect input to be preset
expect(searchInput).toHaveValue(searchFor)
// select only option in dropdown
const options = screen.getAllByRole('option')
expect(options.length).toEqual(2)

// click on the option
fireEvent.click(options[0])

// expect input to be preset
expect(searchInput).toHaveValue(searchFor)
})

it('removes input after selection when reset=true', async () => {
// resolve with no options
mockSearchFn.mockResolvedValueOnce([mockMentionItem, mockMentionItem])
// SET RESET TO FALSE
props.config.reset = true
// remove onCreate fn
props.onCreate = undefined
// render component
render(<FindMention {...props} />)
// input test value
const searchInput = screen.getByRole('combobox')
const searchFor = 'test'
fireEvent.change(searchInput, {target: {value: searchFor}})

// need to advance all timers for debounce etc...
// Note! it needs to be wrapped in act
await act(() => {
jest.runOnlyPendingTimers()
})

const options = screen.getAllByRole('option')
expect(options.length).toEqual(2)
// select option
fireEvent.click(options[0])
// exper input to be reset
expect(searchInput).toHaveValue('')
})
Loading

0 comments on commit 152a7e6

Please sign in to comment.