Skip to content
Open
Show file tree
Hide file tree
Changes from all 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