Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
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
1 change: 1 addition & 0 deletions sippy-ng/src/component_readiness/RegressedTestsPanel.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,7 @@ export default function RegressedTestsPanel(props) {
setFilterModel: setFilterModel,
clearSearch: () => requestSearch(''),
doSearch: requestSearch,
searchField: 'test_name',
autocompleteData: regressedTests,
downloadDataFunc: () => {
return filteredTests
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,7 @@ export default function TriagedRegressionTestList(props) {
setFilterModel: setFilterModel,
clearSearch: () => requestSearch(''),
doSearch: requestSearch,
searchField: 'test_name',
autocompleteData: triagedRegressions,
downloadDataFunc: () => {
return filteredRegressions
Expand Down
1 change: 1 addition & 0 deletions sippy-ng/src/component_readiness/TriagedRegressions.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,7 @@ export default function TriagedRegressions({
setFilterModel: setFilterModel,
clearSearch: () => requestSearch(''),
doSearch: requestSearch,
searchField: 'description',
autocompleteData: triageEntries,
downloadDataFunc: () => {
return filteredTriageEntries
Expand Down
31 changes: 29 additions & 2 deletions sippy-ng/src/datagrid/GridToolbar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import GridToolbarPeriodSelector from '../datagrid/GridToolbarPeriodSelector'
import GridToolbarViewSelector from './GridToolbarViewSelector'
import IconButton from '@mui/material/IconButton'
import PropTypes from 'prop-types'
import React, { Fragment } from 'react'
import React, { Fragment, useEffect, useRef } from 'react'
import SearchIcon from '@mui/icons-material/Search'
import TextField from '@mui/material/TextField'

Expand Down Expand Up @@ -39,6 +39,33 @@ export default function GridToolbar(props) {
const classes = useStyles(theme)

const [search, setSearch] = React.useState('')
const initializedFromFilter = useRef(false)

useEffect(() => {
if (
initializedFromFilter.current ||
!props.searchField ||
!props.filterModel ||
!props.filterModel.items
) {
return
}
const filtersForField = props.filterModel.items.filter(
(f) => f.columnField === props.searchField
)
if (filtersForField.length !== 1) {
return
}
const filter = filtersForField[0]
if (
filter.operatorValue === 'contains' &&
filter.not !== true &&
filter.value
) {
setSearch(filter.value)
initializedFromFilter.current = true
}
}, [props.searchField, props.filterModel])
Comment thread
not-stbenjam marked this conversation as resolved.

return (
<div className={classes.root}>
Expand Down Expand Up @@ -92,7 +119,6 @@ export default function GridToolbar(props) {
value={search}
onChange={(e) => setSearch(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && props.doSearch(search)}
onBlur={() => props.doSearch(search)}
placeholder="Search…"
InputProps={{
endAdornment: (
Expand Down Expand Up @@ -145,4 +171,5 @@ GridToolbar.propTypes = {
downloadDataFunc: PropTypes.func,
downloadFilePrefix: PropTypes.string,
autocompleteData: PropTypes.array,
searchField: PropTypes.string,
}
147 changes: 147 additions & 0 deletions sippy-ng/src/datagrid/GridToolbar.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import '@testing-library/jest-dom'
import { createTheme, ThemeProvider } from '@mui/material/styles'
import { render, screen } from '@testing-library/react'
import GridToolbar from './GridToolbar'
import React from 'react'
import userEvent from '@testing-library/user-event'

vi.mock('@mui/x-data-grid', () => ({
GridToolbarDensitySelector: () => null,
}))

vi.mock('./GridToolbarFilterMenu', () => ({
default: () => null,
}))

const theme = createTheme()

function renderToolbar(props = {}) {
const defaults = {
doSearch: vi.fn(),
clearSearch: vi.fn(),
setFilterModel: vi.fn(),
addFilters: vi.fn(),
filterModel: { items: [] },
}
return render(
<ThemeProvider theme={theme}>
<GridToolbar {...defaults} {...props} />
</ThemeProvider>
)
}

describe('GridToolbar', () => {
describe('search bar initialization from filter', () => {
it('populates search from a single positive contains filter', () => {
renderToolbar({
searchField: 'name',
filterModel: {
items: [
{
columnField: 'name',
operatorValue: 'contains',
value: 'my-job',
},
],
},
})
expect(screen.getByPlaceholderText('Search…')).toHaveValue('my-job')
})

it('does not populate search from a negated contains filter', () => {
renderToolbar({
searchField: 'name',
filterModel: {
items: [
{
columnField: 'name',
operatorValue: 'contains',
not: true,
value: 'excluded',
},
],
},
})
expect(screen.getByPlaceholderText('Search…')).toHaveValue('')
})

it('does not populate search when multiple filters exist for the field', () => {
renderToolbar({
searchField: 'name',
filterModel: {
items: [
{
columnField: 'name',
operatorValue: 'contains',
value: 'a',
},
{
columnField: 'name',
operatorValue: 'contains',
value: 'b',
},
],
},
})
expect(screen.getByPlaceholderText('Search…')).toHaveValue('')
})

it('does not populate search when operator is not contains', () => {
renderToolbar({
searchField: 'name',
filterModel: {
items: [
{
columnField: 'name',
operatorValue: 'equals',
value: 'exact',
},
],
},
})
expect(screen.getByPlaceholderText('Search…')).toHaveValue('')
})
})

describe('search interactions', () => {
it('triggers search on Enter key', async () => {
const doSearch = vi.fn()
renderToolbar({ doSearch })

const input = screen.getByPlaceholderText('Search…')
await userEvent.type(input, 'test-query{enter}')
expect(doSearch).toHaveBeenCalledWith('test-query')
})

it('triggers search on search button click', async () => {
const doSearch = vi.fn()
renderToolbar({ doSearch })

const input = screen.getByPlaceholderText('Search…')
await userEvent.type(input, 'btn-query')
await userEvent.click(screen.getByTitle('Search'))
expect(doSearch).toHaveBeenCalledWith('btn-query')
})

it('does not trigger search on blur', async () => {
const doSearch = vi.fn()
renderToolbar({ doSearch })

const input = screen.getByPlaceholderText('Search…')
await userEvent.type(input, 'blur-query')
await userEvent.tab()
expect(doSearch).not.toHaveBeenCalled()
})

it('clears search on clear button click', async () => {
const clearSearch = vi.fn()
renderToolbar({ clearSearch })

const input = screen.getByPlaceholderText('Search…')
await userEvent.type(input, 'something')
await userEvent.click(screen.getByTitle('Clear'))
expect(clearSearch).toHaveBeenCalled()
expect(input).toHaveValue('')
})
})
})
10 changes: 4 additions & 6 deletions sippy-ng/src/jobs/JobAnalysis.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -261,17 +261,14 @@ export function JobAnalysis(props) {
}

const requestSearch = (searchValue) => {
const currentFilters = testFilter
currentFilters.items = currentFilters.items.filter(
(f) => f.columnField !== 'name'
)
currentFilters.items.push({
const newItems = testFilter.items.filter((f) => f.columnField !== 'name')
newItems.push({
id: 99,
columnField: 'name',
operatorValue: 'contains',
value: searchValue,
})
setTestFilter(currentFilters)
setTestFilter({ ...testFilter, items: newItems })
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

const updateSelectionModel = (m) => {
Expand Down Expand Up @@ -600,6 +597,7 @@ export function JobAnalysis(props) {
setFilterModel: setTestFilter,
clearSearch: () => requestSearch(''),
doSearch: requestSearch,
searchField: 'name',
Comment thread
not-stbenjam marked this conversation as resolved.
},
}}
/>
Expand Down
13 changes: 7 additions & 6 deletions sippy-ng/src/jobs/JobRunsTable.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -461,17 +461,17 @@ export default function JobRunsTable(props) {
}

const requestSearch = (searchValue) => {
const currentFilters = filterModel
currentFilters.items = currentFilters.items.filter(
(f) => f.columnField !== 'job'
)
currentFilters.items.push({
const newItems = filterModel.items.filter((f) => f.columnField !== 'job')
newItems.push({
id: 99,
columnField: 'job',
operatorValue: 'contains',
value: searchValue,
})
setFilterModel(currentFilters)
setFilterModel({
...filterModel,
items: newItems,
})
Comment thread
not-stbenjam marked this conversation as resolved.
}

useEffect(() => {
Expand Down Expand Up @@ -648,6 +648,7 @@ export default function JobRunsTable(props) {
columns: columns,
clearSearch: () => requestSearch(''),
doSearch: requestSearch,
searchField: 'job',
filterModel: filterModel,
setFilterModel: setFilterModel,
addFilters: (m) => addFilters(m),
Expand Down
92 changes: 92 additions & 0 deletions sippy-ng/src/jobs/JobRunsTable.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import '@testing-library/jest-dom'

/**
* The requestSearch pattern used in JobRunsTable (and all other table
* components) immutably replaces the search-field filter while preserving
* other filters. We test the pure logic here because the full component
* requires extensive infrastructure (react-router, query-params, DataGrid,
* API).
*/
function requestSearch(filterModel, searchField, searchValue) {
const newItems = filterModel.items.filter(
(f) => f.columnField !== searchField
)
newItems.push({
id: 99,
columnField: searchField,
operatorValue: 'contains',
value: searchValue,
})
return {
...filterModel,
items: newItems,
}
Comment thread
not-stbenjam marked this conversation as resolved.
}

describe('JobRunsTable requestSearch logic', () => {
it('replaces an existing job filter immutably', () => {
const original = {
items: [
{ id: 1, columnField: 'job', operatorValue: 'contains', value: 'old' },
],
}
const result = requestSearch(original, 'job', 'new-query')
expect(result.items).toHaveLength(1)
expect(result.items[0]).toEqual({
id: 99,
columnField: 'job',
operatorValue: 'contains',
value: 'new-query',
})
expect(original.items).toHaveLength(1)
expect(original.items[0].value).toBe('old')
})

it('preserves filters for other fields', () => {
const original = {
items: [
{
id: 1,
columnField: 'cluster',
operatorValue: 'contains',
value: 'build01',
},
{
id: 2,
columnField: 'job',
operatorValue: 'contains',
value: 'old',
},
],
}
const result = requestSearch(original, 'job', 'new-query')
expect(result.items).toHaveLength(2)
expect(result.items[0].columnField).toBe('cluster')
expect(result.items[0].value).toBe('build01')
expect(result.items[1].value).toBe('new-query')
})

it('adds a filter when none exists for the field', () => {
const original = { items: [] }
const result = requestSearch(original, 'job', 'my-search')
expect(result.items).toHaveLength(1)
expect(result.items[0].value).toBe('my-search')
expect(original.items).toHaveLength(0)
})

it('clears the search by setting an empty value', () => {
const original = {
items: [
{
id: 99,
columnField: 'job',
operatorValue: 'contains',
value: 'prev',
},
],
}
const result = requestSearch(original, 'job', '')
expect(result.items).toHaveLength(1)
expect(result.items[0].value).toBe('')
})
})
Loading