Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
60b590e
Update ActiveFiltersChips with hasSubmitButton prop
alten-dturus Dec 23, 2025
1b1609a
Update filters.type
alten-dturus Dec 23, 2025
3be362a
Update filter.utils
alten-dturus Dec 23, 2025
ea0d711
Added onSetActiveFilters function to useFilters
alten-dturus Dec 23, 2025
bcb466e
Update Filters adding hasSubmitButton prop
alten-dturus Dec 23, 2025
7188f4a
Update FiltersFields adding hasSubmitButton prop and submit and reset…
alten-dturus Dec 23, 2025
3715ef1
Update DatePickerFilterField, FreetextFilterField and NumericFilterFi…
alten-dturus Jan 8, 2026
d749344
Update AutocompleteMultipleFilterField and AutocompleteSingleFilterFi…
alten-dturus Jan 8, 2026
7573fdf
Add new width prop to fields and the disable button check to FilterFi…
alten-dturus Jan 8, 2026
937676c
Add state update on handleRemoveActiveFilter action
alten-dturus Jan 12, 2026
f8d084e
fix test
alten-dturus Jan 12, 2026
37e77c3
Update AutocompleteBaseFilterField adding hasSubmitButton prop and la…
alten-dturus Jan 12, 2026
756dc62
Update AutocompleteBaseFilterField aria-label
alten-dturus Jan 12, 2026
9b37b58
refactor: added breakpoint, refactor method
borgesis95 Mar 24, 2026
b17e235
test: fix test
borgesis95 Mar 25, 2026
033e56c
test: fix test
borgesis95 Mar 26, 2026
43bff33
test: removed only
borgesis95 Mar 26, 2026
b6d248c
refactor: copilot error fixing
borgesis95 Mar 26, 2026
e937e8d
test: add hasSubmitButton flow test coverage (PIN-7358)
Alepazz Mar 31, 2026
8211698
test: add hasSubmitButton integration tests (PIN-9625)
Alepazz Apr 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 157 additions & 0 deletions src/features/filters/__tests__/Filters.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ describe('Filters component', () => {
<Filters
fields={fieldMocks}
activeFilters={[]}
onSetActiveFilters={vi.fn()}
onChangeActiveFilter={vi.fn()}
onRemoveActiveFilter={vi.fn()}
onResetActiveFilters={vi.fn()}
Expand All @@ -75,6 +76,7 @@ describe('Filters component', () => {
<Filters
fields={fieldMocks}
activeFilters={[activeFiltersMocks[0]]}
onSetActiveFilters={vi.fn()}
onChangeActiveFilter={vi.fn()}
onRemoveActiveFilter={vi.fn()}
onResetActiveFilters={vi.fn()}
Expand All @@ -92,6 +94,7 @@ describe('Filters component', () => {
<Filters
fields={fieldMocks}
activeFilters={activeFiltersMocks}
onSetActiveFilters={vi.fn()}
onChangeActiveFilter={vi.fn()}
onRemoveActiveFilter={vi.fn()}
onResetActiveFilters={vi.fn()}
Expand All @@ -109,6 +112,7 @@ describe('Filters component', () => {
<Filters
fields={fieldMocks}
activeFilters={[activeFiltersMocks[0]]}
onSetActiveFilters={vi.fn()}
onChangeActiveFilter={vi.fn()}
onRemoveActiveFilter={vi.fn()}
onResetActiveFilters={vi.fn()}
Expand All @@ -127,6 +131,7 @@ describe('Filters component', () => {
<Filters
fields={fieldMocks}
activeFilters={activeFiltersMocks}
onSetActiveFilters={vi.fn()}
onChangeActiveFilter={vi.fn()}
onRemoveActiveFilter={vi.fn()}
onResetActiveFilters={vi.fn()}
Expand All @@ -145,6 +150,7 @@ describe('Filters component', () => {
<Filters
fields={fieldMocks}
activeFilters={[]}
onSetActiveFilters={vi.fn()}
onChangeActiveFilter={onChangeActiveFilterFn}
onRemoveActiveFilter={vi.fn()}
onResetActiveFilters={vi.fn()}
Expand All @@ -167,6 +173,7 @@ describe('Filters component', () => {
<Filters
fields={fieldMocks}
activeFilters={[]}
onSetActiveFilters={vi.fn()}
onChangeActiveFilter={onChangeActiveFilterFn}
onRemoveActiveFilter={vi.fn()}
onResetActiveFilters={vi.fn()}
Expand Down Expand Up @@ -200,6 +207,7 @@ describe('Filters component', () => {
<Filters
fields={fieldMocks}
activeFilters={activeFiltersMocks}
onSetActiveFilters={vi.fn()}
onChangeActiveFilter={vi.fn()}
onRemoveActiveFilter={onRemoveActiveFilterFn}
onResetActiveFilters={vi.fn()}
Expand All @@ -223,6 +231,7 @@ describe('Filters component', () => {
<Filters
fields={fieldMocks}
activeFilters={activeFiltersMocks}
onSetActiveFilters={vi.fn()}
onChangeActiveFilter={vi.fn()}
onRemoveActiveFilter={vi.fn()}
onResetActiveFilters={onResetActiveFilters}
Expand All @@ -234,3 +243,151 @@ describe('Filters component', () => {
expect(onResetActiveFilters).toBeCalled()
})
})

describe('Filters component with hasSubmitButton', () => {
it('should render "Filtra" and "Annulla filtri" buttons', () => {
const screen = renderWithRouter(
<Filters
fields={fieldMocks}
activeFilters={[]}
onSetActiveFilters={vi.fn()}
onChangeActiveFilter={vi.fn()}
onRemoveActiveFilter={vi.fn()}
onResetActiveFilters={vi.fn()}
hasSubmitButton
/>
)

expect(screen.getByRole('button', { name: 'Filtra' })).toBeInTheDocument()
expect(screen.getByRole('button', { name: 'Annulla filtri' })).toBeInTheDocument()
})

it('should not render submit buttons when hasSubmitButton is not set', () => {
const screen = renderWithRouter(
<Filters
fields={fieldMocks}
activeFilters={[]}
onSetActiveFilters={vi.fn()}
onChangeActiveFilter={vi.fn()}
onRemoveActiveFilter={vi.fn()}
onResetActiveFilters={vi.fn()}
/>
)

expect(screen.queryByRole('button', { name: 'Filtra' })).not.toBeInTheDocument()
})

it('should not apply filters immediately when typing in a freetext field', async () => {
const user = userEvent.setup()
const onChangeActiveFilterFn = vi.fn()

const screen = renderWithRouter(
<Filters
fields={fieldMocks}
activeFilters={[]}
onSetActiveFilters={vi.fn()}
onChangeActiveFilter={onChangeActiveFilterFn}
onRemoveActiveFilter={vi.fn()}
onResetActiveFilters={vi.fn()}
hasSubmitButton
/>
)

const singleFilterField = screen.getByLabelText('Single Filter Field') as HTMLInputElement
await user.type(singleFilterField, 'test-value')
expect(singleFilterField.value).toBe('test-value')
expect(onChangeActiveFilterFn).not.toHaveBeenCalled()
})

it('should not apply filters on Enter key in a freetext field', async () => {
const user = userEvent.setup()
const onChangeActiveFilterFn = vi.fn()

const screen = renderWithRouter(
<Filters
fields={fieldMocks}
activeFilters={[]}
onSetActiveFilters={vi.fn()}
onChangeActiveFilter={onChangeActiveFilterFn}
onRemoveActiveFilter={vi.fn()}
onResetActiveFilters={vi.fn()}
hasSubmitButton
/>
)

const singleFilterField = screen.getByLabelText('Single Filter Field') as HTMLInputElement
await user.type(singleFilterField, 'test-value{enter}')
expect(onChangeActiveFilterFn).not.toHaveBeenCalled()
})

it('should not render search icon in freetext fields', () => {
const screen = renderWithRouter(
<Filters
fields={fieldMocks}
activeFilters={[]}
onSetActiveFilters={vi.fn()}
onChangeActiveFilter={vi.fn()}
onRemoveActiveFilter={vi.fn()}
onResetActiveFilters={vi.fn()}
hasSubmitButton
/>
)

expect(screen.queryByLabelText('Filtra')).not.toBeInTheDocument()
})

it('should call onSetActiveFilters with all field values when clicking "Filtra"', async () => {
const user = userEvent.setup()
const onSetActiveFiltersFn = vi.fn()

const screen = renderWithRouter(
<Filters
fields={fieldMocks}
activeFilters={[]}
onSetActiveFilters={onSetActiveFiltersFn}
onChangeActiveFilter={vi.fn()}
onRemoveActiveFilter={vi.fn()}
onResetActiveFilters={vi.fn()}
hasSubmitButton
/>
)

const singleFilterField = screen.getByLabelText('Single Filter Field') as HTMLInputElement
await user.type(singleFilterField, 'test-value')

const submitButton = screen.getByRole('button', { name: 'Filtra' })
await user.click(submitButton)

expect(onSetActiveFiltersFn).toHaveBeenCalledWith(
fieldMocks,
expect.objectContaining({ 'single-field': 'test-value' })
)
})

it('should reset fields and call onResetActiveFilters when clicking "Annulla filtri"', async () => {
const user = userEvent.setup()
const onResetActiveFiltersFn = vi.fn()

const screen = renderWithRouter(
<Filters
fields={fieldMocks}
activeFilters={[]}
onSetActiveFilters={vi.fn()}
onChangeActiveFilter={vi.fn()}
onRemoveActiveFilter={vi.fn()}
onResetActiveFilters={onResetActiveFiltersFn}
hasSubmitButton
/>
)

const singleFilterField = screen.getByLabelText('Single Filter Field') as HTMLInputElement
await user.type(singleFilterField, 'something')
expect(singleFilterField.value).toBe('something')

const cancelButton = screen.getByRole('button', { name: 'Annulla filtri' })
await user.click(cancelButton)

expect(onResetActiveFiltersFn).toHaveBeenCalled()
expect(singleFilterField.value).toBe('')
})
})
155 changes: 155 additions & 0 deletions src/features/filters/__tests__/FiltersIntegration.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import React from 'react'
import { describe, expect, it } from 'vitest'
import userEvent from '@testing-library/user-event'
import { render, screen, waitFor } from '@testing-library/react'
import { MemoryRouter, Route, Routes, useSearchParams } from 'react-router-dom'
import { Filters } from '../components/Filters'
import { useFilters } from '../hooks/useFilters'
import type { FilterFields, FiltersParams } from '../filters.types'

const fieldMocks: FilterFields = [
{ name: 'q', type: 'freetext', label: 'Name' },
{
name: 'status',
type: 'autocomplete-multiple',
options: [
{ label: 'Active', value: 'active' },
{ label: 'Suspended', value: 'suspended' },
],
label: 'Status',
},
]

/**
* Integration wrapper that wires useFilters + Filters together,
* matching the real usage pattern in the frontend.
* Includes a hidden element exposing current URL search params for assertions.
*/
const FiltersWrapper: React.FC<{ hasSubmitButton?: boolean }> = ({ hasSubmitButton }) => {
const { filtersParams: _filtersParams, ...filtersHandlers } =
useFilters<FiltersParams>(fieldMocks)
const [searchParams] = useSearchParams()
return (
<>
<Filters {...filtersHandlers} hasSubmitButton={hasSubmitButton} />
<div data-testid="search-params">{searchParams.toString()}</div>
</>
)
}

function renderFiltersWrapper(hasSubmitButton = true, initialSearch?: string) {
const initialEntries = initialSearch ? [`/?${initialSearch}`] : ['/']
return render(
<MemoryRouter initialEntries={initialEntries}>
<Routes>
<Route path="*" element={<FiltersWrapper hasSubmitButton={hasSubmitButton} />} />
</Routes>
</MemoryRouter>
)
}

function getSearchParams(): string {
return screen.getByTestId('search-params').textContent ?? ''
}

describe('Filters integration with hasSubmitButton', () => {
it('should have "Filtra" and "Annulla filtri" buttons disabled when no filter value is entered', () => {
renderFiltersWrapper()

expect(screen.getByRole('button', { name: 'Filtra' })).toBeDisabled()
expect(screen.getByRole('button', { name: 'Annulla filtri' })).toBeDisabled()
})

it('should enable buttons when a filter value is entered', async () => {
const user = userEvent.setup()
renderFiltersWrapper()

await user.type(screen.getByLabelText('Name'), 'test')

expect(screen.getByRole('button', { name: 'Filtra' })).toBeEnabled()
expect(screen.getByRole('button', { name: 'Annulla filtri' })).toBeEnabled()
})

it('should update URL params and show chip after clicking "Filtra"', async () => {
const user = userEvent.setup()
renderFiltersWrapper()

await user.type(screen.getByLabelText('Name'), 'test-value')
await user.click(screen.getByRole('button', { name: 'Filtra' }))

await waitFor(() => {
expect(getSearchParams()).toContain('q=test-value')
})

expect(screen.getByText('test-value')).toBeInTheDocument()
})

it('should remove chip and URL param when a chip is deleted', async () => {
const user = userEvent.setup()
renderFiltersWrapper(true, 'q=existing-filter')

expect(screen.getByText('existing-filter')).toBeInTheDocument()
expect(getSearchParams()).toContain('q=existing-filter')

await user.click(screen.getByTestId('CancelIcon'))

await waitFor(() => {
expect(getSearchParams()).not.toContain('q=')
})

expect(screen.queryByText('existing-filter')).not.toBeInTheDocument()
})

it('should clear URL params when clicking "Annulla filtri" after submitting a filter', async () => {
const user = userEvent.setup()
renderFiltersWrapper()

// Type and submit
await user.type(screen.getByLabelText('Name'), 'some-filter')
await user.click(screen.getByRole('button', { name: 'Filtra' }))

await waitFor(() => {
expect(getSearchParams()).toContain('q=some-filter')
})
expect(screen.getByText('some-filter')).toBeInTheDocument()

// Type something new to re-enable the buttons, then cancel
await user.type(screen.getByLabelText('Name'), 'x')
await user.click(screen.getByRole('button', { name: 'Annulla filtri' }))

await waitFor(() => {
expect(getSearchParams()).not.toContain('q=')
})

expect((screen.getByLabelText('Name') as HTMLInputElement).value).toBe('')
})

it('should delete offset from URL when submitting filters', async () => {
const user = userEvent.setup()
renderFiltersWrapper(true, 'offset=20')

expect(getSearchParams()).toContain('offset=20')

await user.type(screen.getByLabelText('Name'), 'test')
await user.click(screen.getByRole('button', { name: 'Filtra' }))

await waitFor(() => {
expect(getSearchParams()).not.toContain('offset=')
})
expect(getSearchParams()).toContain('q=test')
})

it('should delete offset from URL when removing a filter chip', async () => {
const user = userEvent.setup()
renderFiltersWrapper(true, 'q=test&offset=20')

expect(getSearchParams()).toContain('offset=20')

await user.click(screen.getByTestId('CancelIcon'))

await waitFor(() => {
expect(getSearchParams()).not.toContain('offset=')
})
expect(getSearchParams()).not.toContain('q=')
})
})
Loading
Loading