Skip to content
Merged
16 changes: 16 additions & 0 deletions src/components/pluginList/PluginSearchBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import TextField from '@mui/material/TextField';
import InputAdornment from '@mui/material/InputAdornment';
import IconButton from '@mui/material/IconButton';
import SearchIcon from '@mui/icons-material/Search';
import ClearIcon from '@mui/icons-material/Clear';
Comment thread
PratikMane0112 marked this conversation as resolved.
Resolved
import { alpha } from '@mui/material/styles';
import type { PluginStatusColor } from '../../types';
import { colors, statusColorMap, STATUS_CARD_DEFS } from '../../theme';
Expand Down Expand Up @@ -36,6 +38,20 @@ export default function PluginSearchBar({
<SearchIcon sx={{ color: colors.text.muted, fontSize: 20 }} />
</InputAdornment>
),
endAdornment: search ? (
<InputAdornment position="end">
<IconButton
size="small"
onClick={() => onSearchChange('')}
onMouseDown={(e) => e.preventDefault()}
aria-label="Clear search input"
edge="end"
sx={{ color: colors.text.muted, p: 0.5 }}
Comment thread
hrithik577 marked this conversation as resolved.
Resolved
>
<ClearIcon sx={{ fontSize: 16 }} />
</IconButton>
</InputAdornment>
) : null,
},
}}
sx={{
Expand Down
18 changes: 16 additions & 2 deletions src/components/recipeList/RecipeSearchBar.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Box, Typography, TextField, InputAdornment } from '@mui/material';
import { Box, Typography, TextField, InputAdornment, IconButton } from '@mui/material';
import { alpha } from '@mui/material/styles';
import type { RateTier } from '../../util/recipeStatus';
import { RATE_CARD_DEFS } from '../../util/recipeStatus';
import { colors, rateTierColorMap } from '../../theme';
import { SearchOutlined } from '@mui/icons-material';
import { SearchOutlined, Clear as ClearIcon } from '@mui/icons-material';

interface RecipeSearchBarProps {
search: string;
Expand Down Expand Up @@ -34,6 +34,20 @@ export default function RecipeSearchBar({
<SearchOutlined sx={{ color: colors.text.muted, fontSize: 20 }} />
</InputAdornment>
),
endAdornment: search ? (
<InputAdornment position="end">
<IconButton
size="small"
onClick={() => onSearchChange('')}
onMouseDown={(e) => e.preventDefault()}
aria-label="Clear search input"
edge="end"
sx={{ color: colors.text.muted, p: 0.5 }}
Comment thread
krisstern marked this conversation as resolved.
Resolved
>
<ClearIcon sx={{ fontSize: 16 }} />
</IconButton>
</InputAdornment>
) : null,
},
}}
sx={{
Expand Down
111 changes: 111 additions & 0 deletions tests/components/pluginList/PluginSearchBar.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import React from 'react';
import { describe, it, expect, vi } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import PluginSearchBar from '../../../src/components/pluginList/PluginSearchBar';

describe('PluginSearchBar', () => {
it('renders search input with placeholder', () => {
render(
<PluginSearchBar
search=""
onSearchChange={() => {}}
statusFilter="all"
onClearFilter={() => {}}
resultCount={42}
/>
);
expect(screen.getByPlaceholderText('Search a plugin…')).toBeDefined();
console.log(` PluginSearchBar : search input with placeholder rendered`);
});

it('displays result count correctly', () => {
render(
<PluginSearchBar
search=""
onSearchChange={() => {}}
statusFilter="all"
onClearFilter={() => {}}
resultCount={42}
/>
);
expect(screen.getByText('42 results')).toBeDefined();
console.log(` PluginSearchBar : "42 results" displayed`);
});

it('displays singular "result" when count is 1', () => {
render(
<PluginSearchBar
search=""
onSearchChange={() => {}}
statusFilter="all"
onClearFilter={() => {}}
resultCount={1}
/>
);
expect(screen.getByText('1 result')).toBeDefined();
console.log(` PluginSearchBar : "1 result" (singular) displayed`);
});

it('calls onSearchChange when typing in search input', () => {
const onChange = vi.fn();
render(
<PluginSearchBar
search=""
onSearchChange={onChange}
statusFilter="all"
onClearFilter={() => {}}
resultCount={0}
/>
);
fireEvent.change(screen.getByPlaceholderText('Search a plugin…'), {
target: { value: 'git' },
});
expect(onChange).toHaveBeenCalledWith('git');
console.log(` PluginSearchBar : typing "git" triggers onSearchChange`);
});

it('shows status filter chip when statusFilter is not "all"', () => {
render(
<PluginSearchBar
search=""
onSearchChange={() => {}}
statusFilter="green"
onClearFilter={() => {}}
resultCount={10}
/>
);
expect(screen.getByText('All Passed')).toBeDefined();
expect(screen.getByText('✕')).toBeDefined();
console.log(` PluginSearchBar : "All Passed" status chip rendered`);
});

it('calls onClearFilter when status filter chip dismiss is clicked', () => {
const onClear = vi.fn();
render(
<PluginSearchBar search="" onSearchChange={() => {}} statusFilter="red" onClearFilter={onClear} resultCount={5} />
);
fireEvent.click(screen.getByText('✕').closest('button')!);
expect(onClear).toHaveBeenCalled();
console.log(` PluginSearchBar : clicking status filter dismiss calls onClearFilter`);
});

it('renders search clear button when search is non-empty and triggers onSearchChange("") when clicked', () => {
const onSearchChange = vi.fn();
render(
<PluginSearchBar
search="git"
onSearchChange={onSearchChange}
statusFilter="all"
onClearFilter={() => {}}
resultCount={5}
/>
);

const clearButton = screen.getByRole('button', { name: 'Clear search input' });
expect(clearButton).toBeDefined();

fireEvent.click(clearButton);
expect(onSearchChange).toHaveBeenCalledWith('');
console.log(` PluginSearchBar : clicking clear input button triggers onSearchChange("")`);
});
});
20 changes: 20 additions & 0 deletions tests/components/recipeList/RecipeSearchBar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,4 +78,24 @@ describe('RecipeSearchBar', () => {
expect(onClear).toHaveBeenCalled();
console.log(` RecipeSearchBar : clicking dismiss calls onClearFilter`);
});

it('renders search clear button when search is non-empty and triggers onSearchChange("") when clicked', () => {
const onSearchChange = vi.fn();
render(
<RecipeSearchBar
search="Setup"
onSearchChange={onSearchChange}
tierFilter="all"
onClearFilter={() => {}}
resultCount={5}
/>
);

const clearButton = screen.getByRole('button', { name: 'Clear search input' });
expect(clearButton).toBeDefined();

fireEvent.click(clearButton);
expect(onSearchChange).toHaveBeenCalledWith('');
console.log(` RecipeSearchBar : clicking clear input button triggers onSearchChange("")`);
});
});
4 changes: 2 additions & 2 deletions tests/pages/PluginList.integration.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ function countByStatus(plugins: PluginReport[]): Record<PluginStatusColor, numbe
return counts;
}

describe('PluginList integration (real report.json)', () => {
describe('PluginList integration (real report.json)', { timeout: 30000 }, () => {
let realReport: ReportJson;
let expectedPlugins: PluginReport[];
let expectedCounts: Record<PluginStatusColor, number>;
Expand Down Expand Up @@ -98,7 +98,7 @@ describe('PluginList integration (real report.json)', () => {
});
}

it('renders the correct total plugin count from real data', async () => {
it('renders the correct total plugin count from real data', { timeout: 15000 }, async () => {
await renderAndWait();

expect(screen.getByText('Plugins')).toBeDefined();
Expand Down