Skip to content

Commit

Permalink
test(SearchInput): added test & updated jest config
Browse files Browse the repository at this point in the history
  • Loading branch information
charmi-v committed Jul 26, 2024
1 parent 975bacd commit 5361d94
Show file tree
Hide file tree
Showing 10 changed files with 160 additions and 15 deletions.
9 changes: 9 additions & 0 deletions jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,13 @@ module.exports = {
transform: {
'^.+\\.tsx?$': 'ts-jest',
},
moduleNameMapper: {
'\\.(css|scss)$': '<rootDir>/src/test/__mocks__/styleMock.ts',
'\\.svg$': '<rootDir>/src/test/__mocks__/fileMock.ts',
'@mui/material/styles/createPalette':
'<rootDir>/src/test/__mocks__/createPalette.ts',
'@mui/material/styles/createTypography':
'<rootDir>/src/test/__mocks__/createTypography.ts',
},
setupFiles: ['<rootDir>/src/test/setupTests.ts'],
}
12 changes: 2 additions & 10 deletions src/components/basic/SearchInput/SearchInput.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,23 +37,15 @@ const Template: ComponentStory<typeof Component> = (args: any) => (
export const SearchInput = Template.bind({})
SearchInput.args = {}

export const ControlledSearchInputWithDebounce = () => {
export const SearchInputWithDebounce = () => {
const [term, setTerm] = useState('')

return (
<Component
debounceTimeout={500}
value={term}
onDebouncedChange={(v) => console.log('onDebouncedChange', v)}
onSearch={(v) => console.log('onSearch', v)}
onChange={(e) => setTerm(e.target.value)}
/>
)
}
export const UncontrolledSearchInputWithDebounce = () => {
return (
<Component
debounceTimeout={500}
onDebouncedChange={(v) => console.log('onDebouncedChange', v)}
/>
)
}
106 changes: 106 additions & 0 deletions src/components/basic/SearchInput/SearchInput.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { useState } from 'react'
import { fireEvent, screen, waitFor } from '@testing-library/react'
import { SearchInput } from '.'
import { render } from '../../../test/test-utils'

jest.useFakeTimers()

describe('SearchInput', () => {
const debounceTimeout = 300
const onSearch = jest.fn()
const onChange = jest.fn()

const ControlledSearchInput = () => {
const [value, setValue] = useState('')
return (
<SearchInput
debounceTimeout={debounceTimeout}
onSearch={onSearch}
value={value}
onChange={(e) => setValue(e.target.value)}
placeholder="Search"
/>
)
}
const UncontrolledSearchInput = () => (
<SearchInput onSearch={onSearch} onChange={onChange} placeholder="Search" />
)

beforeEach(() => {
jest.clearAllMocks()
})

test('renders the Search Input component', () => {
render(<SearchInput />)
})

test('should call search function after specified debounce timeout', async () => {
render(<UncontrolledSearchInput />)
const input = screen.getByPlaceholderText('Search')
fireEvent.change(input, { target: { value: 'te' } })
fireEvent.change(input, { target: { value: 'testing...' } })
expect(onSearch).not.toHaveBeenCalled()
expect(onChange).toHaveBeenCalledTimes(2)

jest.advanceTimersByTime(debounceTimeout)
await waitFor(() => expect(onSearch).toHaveBeenCalledWith('testing...'))
expect(onSearch).toHaveBeenCalledTimes(1)
})

test('should call onChange handler when input value changes', () => {
render(<UncontrolledSearchInput />)

const input = screen.getByPlaceholderText('Search')
fireEvent.change(input, { target: { value: 't' } })
expect(onChange).toHaveBeenCalledTimes(1)
fireEvent.change(input, { target: { value: 'te' } })
expect(onChange).toHaveBeenCalledTimes(2)
fireEvent.change(input, { target: { value: 'test' } })
expect(onChange).toHaveBeenCalledTimes(3)
})

test('should call debounced search function with new value after reset', async () => {
render(<ControlledSearchInput />)

const input = screen.getByPlaceholderText('Search')
fireEvent.change(input, { target: { value: 'test' } })

jest.advanceTimersByTime(debounceTimeout)
await waitFor(() => expect(onSearch).toHaveBeenCalledWith('test'))

fireEvent.change(input, { target: { value: '' } })

jest.advanceTimersByTime(debounceTimeout)
await waitFor(() => expect(onSearch).toHaveBeenCalledWith(''))
})

test('should not call search function on initial render when value is provided', () => {
render(<ControlledSearchInput />)

jest.advanceTimersByTime(debounceTimeout)
expect(onChange).not.toHaveBeenCalled()
})

test('should call search function when controlled value changes', async () => {
const { rerender } = render(
<SearchInput
debounceTimeout={debounceTimeout}
onSearch={onSearch}
value="initial"
onChange={onChange}
/>
)

rerender(
<SearchInput
debounceTimeout={debounceTimeout}
onSearch={onSearch}
value="updated"
onChange={onChange}
/>
)

jest.advanceTimersByTime(debounceTimeout)
await waitFor(() => expect(onSearch).toHaveBeenCalledWith('updated'))
})
})
9 changes: 4 additions & 5 deletions src/components/basic/SearchInput/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,12 @@ interface SearchProps extends Omit<TextFieldProps, 'variant'> {
variant?: 'outlined'
endAdornment?: React.ReactNode
debounceTimeout?: number
onDebouncedChange?: (value: string) => void
onSearch?: (value: string) => void
}

export const SearchInput = ({
debounceTimeout = 0,
onDebouncedChange,
onSearch,
variant,
endAdornment,
...props
Expand All @@ -55,8 +55,7 @@ export const SearchInput = ({
// Memoize the debounced function
const debouncedSearch = useCallback(
debounce((query: string) => {
console.log({ value, query })
onDebouncedChange?.(query)
onSearch?.(query)
}, debounceTimeout),
[]
)
Expand All @@ -72,7 +71,7 @@ export const SearchInput = ({
const newValue = e.target.value

// Handle debounce when input is uncontrolled
if (value === undefined && onDebouncedChange) {
if (value === undefined) {
debouncedSearch(newValue)
}
onChange?.(e)
Expand Down
3 changes: 3 additions & 0 deletions src/test/__mocks__/createPalette.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { type Palette } from '@mui/material'
const createPalette: (palette: Palette) => Palette = (palette) => palette
export default createPalette
11 changes: 11 additions & 0 deletions src/test/__mocks__/createTypography.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { type Palette } from '@mui/material'
import { type TypographyOptions } from '@mui/material/styles/createTypography'

const createTypography: (
palette: Palette,
typography: TypographyOptions
) => TypographyOptions = (palette, typography) => ({
...palette,
...typography,
})
export default createTypography
3 changes: 3 additions & 0 deletions src/test/__mocks__/fileMock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = {
default: 'default-mocking-for-file',
}
1 change: 1 addition & 0 deletions src/test/__mocks__/styleMock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = {}
9 changes: 9 additions & 0 deletions src/test/setupTests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
window.matchMedia =
window.matchMedia ||
function () {
return {
matches: false,
addListener: function () {},
removeListener: function () {},
}
}
12 changes: 12 additions & 0 deletions src/test/test-utils.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import React, { type ReactElement } from 'react'
import { SharedThemeProvider } from '../components'
import { render as rtlRender, type RenderOptions } from '@testing-library/react'

const AllTheProviders = ({ children }: { children: React.ReactNode }) => {
return <SharedThemeProvider>{children}</SharedThemeProvider>
}

export const render = (
ui: ReactElement,
options?: Omit<RenderOptions, 'wrapper'>
) => rtlRender(ui, { wrapper: AllTheProviders, ...options })

0 comments on commit 5361d94

Please sign in to comment.