Skip to content
Closed
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
2 changes: 2 additions & 0 deletions modules/ui/src/app/model/filters.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ describe('Filters Models', () => {

describe('Enums', () => {
it('should define FilterItem values correctly', () => {
expect(FilterItem.QuickSearch).toBe('Raw search');
expect(FilterItem.DeviceInfo).toBe('Device');
expect(FilterItem.DeviceFirmware).toBe('Firmware');
expect(FilterItem.Results).toBe('Result');
Expand All @@ -123,6 +124,7 @@ describe('Filters Models', () => {
});

it('should define FilterTitle values correctly', () => {
expect(FilterTitle.QuickSearch).toBe('Enter raw search');
expect(FilterTitle.DeviceInfo).toBe('Enter device name');
expect(FilterTitle.DeviceFirmware).toBe('Enter firmware name');
expect(FilterTitle.Results).toBe('Select status');
Expand Down
2 changes: 2 additions & 0 deletions modules/ui/src/app/model/filters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export interface FilterMenuItem {
}

export enum FilterItem {
QuickSearch = 'Raw search',
DeviceInfo = 'Device',
DeviceFirmware = 'Firmware',
Results = 'Result',
Expand All @@ -61,6 +62,7 @@ export enum FilterName {
}

export enum FilterTitle {
QuickSearch = 'Enter raw search',
DeviceInfo = 'Enter device name',
DeviceFirmware = 'Enter firmware name',
Results = 'Select status',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,16 @@ <h2 class="filter-dialog-title" mat-dialog-title>{{ data.title }}</h2>
'max-height': 'calc(100vh - ' + (topPosition + dialogTitle) + 'px)',
}">
<form [formGroup]="filterForm" class="filter-form">
@if (data.filter === FilterName.QuickSearch) {
<mat-form-field appearance="outline" class="text-field">
<mat-label>Raw search</mat-label>
<input
class="text-field-input raw-search-input"
formControlName="quickSearch"
matInput
aria-label="raw search" />
</mat-form-field>
}
@if (data.filter === FilterName.DeviceInfo) {
<mat-form-field appearance="outline" class="text-field">
<mat-label>Device</mat-label>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ describe('FilterDialogComponent', () => {
fixture.detectChanges();
component.filterForm.get('deviceInfo')?.setValue('deviceInfo ');
const mockFormData = {
quickSearch: '',
deviceInfo: 'deviceInfo',
deviceFirmware: '',
location: '',
Expand Down Expand Up @@ -144,6 +145,7 @@ describe('FilterDialogComponent', () => {
component.results.controls[0].setValue(true);

const mockFormData = {
quickSearch: '',
deviceInfo: '',
deviceFirmware: '',
location: 'Data Center 1',
Expand All @@ -163,6 +165,35 @@ describe('FilterDialogComponent', () => {
expect(closeSpy).toHaveBeenCalledWith(mockFormData);
});

it('should close dialog with raw search on "confirm" click', () => {
component.data = {
trigger: mockClientRest,
filter: FilterName.QuickSearch,
title: FilterTitle.QuickSearch,
};
fixture.detectChanges();
component.filterForm.get('quickSearch')?.setValue(' my raw query ');
const mockFormData = {
quickSearch: 'my raw query',
deviceInfo: '',
deviceFirmware: '',
location: '',
linuxEnv: '',
pythonVersion: '',
kernel: '',
results: [],
dateRange: new DateRange(),
};
const closeSpy = spyOn(component.dialogRef, 'close');
const confirmButton = compiled.querySelector(
'.confirm-button'
) as HTMLButtonElement;

confirmButton?.click();

expect(closeSpy).toHaveBeenCalledWith(mockFormData);
});

it('should auto-populate end date with today if start date is set and end date is empty on confirm', () => {
fixture.detectChanges();
const startDate = new Date('2024-01-01');
Expand Down Expand Up @@ -199,6 +230,9 @@ describe('FilterDialogComponent', () => {

it('should provide form control getters', () => {
fixture.detectChanges();
expect(component.quickSearch).toBe(
component.filterForm.get('quickSearch')!
);
expect(component.deviceInfo).toBe(component.filterForm.get('deviceInfo')!);
expect(component.deviceFirmware).toBe(
component.filterForm.get('deviceFirmware')!
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,10 @@ export class FilterDialogComponent
this.dialogRef = dialogRef;
}

get quickSearch() {
return this.filterForm.get('quickSearch') as AbstractControl;
}

get deviceInfo() {
return this.filterForm.get('deviceInfo') as AbstractControl;
}
Expand Down Expand Up @@ -238,6 +242,7 @@ export class FilterDialogComponent
}
private createFilterForm() {
this.filterForm = this.fb.group({
quickSearch: ['', []],
deviceInfo: ['', [this.deviceValidators.deviceStringFormat()]],
deviceFirmware: ['', [this.deviceValidators.firmwareStringFormat()]],
location: ['', []],
Expand Down Expand Up @@ -293,6 +298,7 @@ export class FilterDialogComponent
}

const filtersData = {
quickSearch: formData.quickSearch?.trim() || '',
deviceInfo: formData.deviceInfo?.trim() || '',
deviceFirmware: formData.deviceFirmware?.trim() || '',
location: formData.location?.trim() || '',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="search-field" [class.focused]="isFocused">
<div class="search-field">
<button
type="button"
mat-icon-button
Expand Down Expand Up @@ -62,22 +62,9 @@
</button>
</div>
}

<input
#searchInput
type="text"
class="search-input"
[placeholder]="'Add a filter'"
[value]="inputValue"
(input)="onInputChange($event)"
(keydown.enter)="onEnter($event)"
(keydown.backspace)="onBackspace()"
(focus)="isFocused = true"
(blur)="isFocused = false"
aria-label="Search reports" />
</div>

@if (inputValue || hasActiveFilters()) {
@if (hasActiveFilters()) {
<button
type="button"
mat-button
Expand All @@ -94,7 +81,7 @@
class="search-filter-menu"
yPosition="below"
[overlapTrigger]="false">
@for (menuItem of filterMenuItems; track menuItem) {
@for (menuItem of filterMenuItems; track menuItem.name) {
<button
mat-menu-item
class="filter-menu-item"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
padding: 4px 8px 4px 6px;
box-sizing: border-box;
width: 100%;
cursor: text;
}

.search-menu-trigger {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { SearchComponent } from './search.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import {
DateRange,
FilterItem,
FilterName,
FilterTitle,
Filters,
Expand Down Expand Up @@ -87,7 +88,7 @@ describe('SearchComponent', () => {
it('should format chip labels correctly for all filter types', () => {
expect(
component.getFilterChipLabel(FilterName.QuickSearch, 'query')
).toBe('search: "query"');
).toBe('Raw search contains "query"');
expect(
component.getFilterChipLabel(FilterName.DeviceInfo, 'Device A')
).toBe('Device contains "Device A"');
Expand Down Expand Up @@ -155,7 +156,7 @@ describe('SearchComponent', () => {

it('should generate accessible labels', () => {
expect(component.getChipAriaLabel(FilterName.QuickSearch, 'test')).toBe(
'Filter: search: "test". Click to edit.'
'Filter: Raw search contains "test". Click to edit.'
);
expect(
component.getRemoveFilterAriaLabel(FilterName.DeviceInfo, 'Pixel')
Expand Down Expand Up @@ -290,7 +291,6 @@ describe('SearchComponent', () => {
it('should clear all filters on clearAll and handle event', () => {
spyOn(component.filterCleared, 'emit');
spyOn(component.searchQueryChanged, 'emit');
component.inputValue = 'some text';

const event = new MouseEvent('click');
spyOn(event, 'preventDefault');
Expand All @@ -300,7 +300,6 @@ describe('SearchComponent', () => {

expect(event.preventDefault).toHaveBeenCalled();
expect(event.stopPropagation).toHaveBeenCalled();
expect(component.inputValue).toBe('');
expect(component.filters.deviceInfo).toBe('');
expect(component.filters.deviceFirmware).toBe('');
expect(component.filters.results).toEqual([]);
Expand All @@ -317,110 +316,15 @@ describe('SearchComponent', () => {
});
});

describe('Input handling', () => {
it('should update inputValue on input change', () => {
const event = {
target: { value: 'my search' },
} as unknown as Event;

component.onInputChange(event);
expect(component.inputValue).toBe('my search');
});

it('should add search chip on Enter and clear input', () => {
spyOn(component.filterCleared, 'emit');
spyOn(component.searchQueryChanged, 'emit');
component.inputValue = ' Raspberry Pi ';

const event = new KeyboardEvent('keydown', { key: 'Enter' });
spyOn(event, 'preventDefault');

component.onEnter(event);

expect(event.preventDefault).toHaveBeenCalled();
expect(component.filters.quickSearch).toBe('Raspberry Pi');
expect(component.searchQueryChanged.emit).toHaveBeenCalledWith(
'Raspberry Pi'
);
expect(component.filterCleared.emit).toHaveBeenCalledWith(
component.filters
);
expect(component.inputValue).toBe('');
});

it('should not add search chip on Enter if input is empty or whitespace', () => {
spyOn(component.filterCleared, 'emit');
spyOn(component.searchQueryChanged, 'emit');
component.inputValue = ' ';

const event = new KeyboardEvent('keydown', { key: 'Enter' });
component.onEnter(event);

expect(component.searchQueryChanged.emit).not.toHaveBeenCalled();
expect(component.filterCleared.emit).not.toHaveBeenCalled();
});

it('should remove last filter on Backspace when input is empty', () => {
component.filters = {
deviceInfo: 'Pixel',
deviceFirmware: '1.0',
results: [],
dateRange: '',
quickSearch: '',
location: '',
linuxEnv: '',
pythonVersion: '',
kernel: '',
};
component.inputValue = '';
spyOn(component, 'removeFilter');

component.onBackspace();

expect(component.removeFilter).toHaveBeenCalledWith('deviceFirmware');
});

it('should not remove last filter on Backspace when no active filters', () => {
component.filters = new Filters();
component.inputValue = '';
spyOn(component, 'removeFilter');

component.onBackspace();

expect(component.removeFilter).not.toHaveBeenCalled();
});

it('should not remove last filter on Backspace if input is not empty', () => {
component.filters = {
deviceInfo: 'Pixel',
deviceFirmware: '',
results: [],
dateRange: '',
quickSearch: '',
location: '',
linuxEnv: '',
pythonVersion: '',
kernel: '',
};
component.inputValue = 'abc';
spyOn(component, 'removeFilter');

component.onBackspace();

expect(component.removeFilter).not.toHaveBeenCalled();
});

it('should focus search input on focusInput()', () => {
const inputEl = component.searchInput()?.nativeElement;
if (inputEl) {
spyOn(inputEl, 'focus');
component.focusInput();
expect(inputEl.focus).toHaveBeenCalled();
}
describe('Filter menu & dialog opening', () => {
it('should have Raw search as the first item in filterMenuItems', () => {
expect(component.filterMenuItems[0]).toEqual({
displayName: FilterItem.QuickSearch,
name: FilterName.QuickSearch,
title: FilterTitle.QuickSearch,
});
});
});

describe('Filter menu & dialog opening', () => {
it('should emit emitOpenFilter on openFilterMenu for all menu items', () => {
spyOn(component.emitOpenFilter, 'emit');
component.filterMenuItems.forEach(item => {
Expand Down Expand Up @@ -467,11 +371,8 @@ describe('SearchComponent', () => {
});

describe('DOM rendering', () => {
it('should render search input and filter button', () => {
const input = compiled.querySelector('.search-input');
it('should render filter button', () => {
const filterBtn = compiled.querySelector('.filter-button');

expect(input).toBeTruthy();
expect(filterBtn).toBeTruthy();
});

Expand All @@ -493,13 +394,12 @@ describe('SearchComponent', () => {
expect(chips.length).toBe(3);
});

it('should render clear button when filters or input value are present', () => {
it('should render clear button when filters are present', () => {
component.filters = new Filters();
component.inputValue = '';
fixture.detectChanges();
expect(compiled.querySelector('.clear-button')).toBeNull();

component.inputValue = 'abc';
component.filters.quickSearch = 'test';
fixture.detectChanges();
const clearBtn = compiled.querySelector('.clear-button');
expect(clearBtn).toBeTruthy();
Expand Down
Loading
Loading