Skip to content

Commit

Permalink
feat(filters): add few more Filters
Browse files Browse the repository at this point in the history
  • Loading branch information
ghiscoding-SE committed Mar 24, 2020
1 parent 6ce876e commit 76b4177
Show file tree
Hide file tree
Showing 22 changed files with 1,357 additions and 37 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,8 @@ npm run test:watch
- [ ] Filters
- [ ] Autocomplete
- [ ] Compound Date
- [ ] Compound Input(s)
- [ ] Compound Slider
- [x] Compound Input(s)
- [x] Compound Slider
- [ ] Date Range
- [x] Input(s)
- [x] Multiple Select
Expand Down
4 changes: 2 additions & 2 deletions packages/common/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@
"postbundle": "npm-run-all sass:build sass:copy",
"bundle:amd": "cross-env tsc --project tsconfig.build.json --outDir dist/amd --module amd",
"bundle:commonjs": "tsc --project tsconfig.build.json --outDir dist/commonjs --module commonjs",
"bundle:es2015": "cross-env tsc --project tsconfig.build.json --outDir dist/es2015 --module es2015 --target es2015",
"bundle:es2020": "cross-env tsc --project tsconfig.build.json --outDir dist/es2020 --module es2015 --target es2020",
"bundle:es2015": "cross-env tsc --project tsconfig.build.json --outDir dist/es2015 --module es2020 --target es2015",
"bundle:es2020": "cross-env tsc --project tsconfig.build.json --outDir dist/es2020 --module es2020 --target es2020",
"bundle:native-modules": "cross-env tsc --project tsconfig.build.json --outDir dist/native-modules --module es2015",
"bundle:system": "cross-env tsc --project tsconfig.build.json --outDir dist/system --module system",
"delete:dist": "cross-env rimraf dist",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,10 @@ describe('checkboxSelectorExtension', () => {
let columnsMock: Column[];

beforeEach(() => {
columnsMock = [{ id: 'field1', field: 'field1', width: 100, cssClass: 'red' }];
columnsMock = [
{ id: 'field1', field: 'field1', width: 100, cssClass: 'red' },
{ id: 'field2', field: 'field2', width: 50 }
];
columnSelectionMock = { id: '_checkbox_selector', field: 'sel' };
jest.spyOn(SharedService.prototype, 'grid', 'get').mockReturnValue(gridStub);
jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(gridOptionsMock);
Expand Down Expand Up @@ -123,6 +126,36 @@ describe('checkboxSelectorExtension', () => {

expect(mockSelectionModel).toHaveBeenCalledWith(optionMock);
expect(columnsMock[0]).toEqual(selectionColumn);
expect(columnsMock).toEqual([
{ excludeFromColumnPicker: true, excludeFromExport: true, excludeFromGridMenu: true, excludeFromHeaderMenu: true, excludeFromQuery: true, field: 'sel', id: '_checkbox_selector', },
{ cssClass: 'red', field: 'field1', id: 'field1', width: 100, },
{ field: 'field2', id: 'field2', width: 50, }
]);
});

it('should be able to change the position of the checkbox column to another column index position in the grid', () => {
const rowSelectionOptionMock = { selectActiveRow: true };
gridOptionsMock.checkboxSelector = { columnIndexPosition: 2, };
const selectionModelOptions = { ...gridOptionsMock, rowSelectionOptions: rowSelectionOptionMock };
const selectionColumn = { ...columnSelectionMock, excludeFromExport: true, excludeFromColumnPicker: true, excludeFromGridMenu: true, excludeFromQuery: true, excludeFromHeaderMenu: true };
jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(selectionModelOptions);

// we can only spy after 1st "create" call, we'll only get a valid selectionColumn on 2nd "create" call
const instance = extension.create(columnsMock, gridOptionsMock);
jest.spyOn(instance, 'getColumnDefinition').mockReturnValue(columnSelectionMock);
expect(columnsMock[0]).not.toEqual(selectionColumn);

// do our expect here after 2nd "create" call, the selectionColumn flags will change only after this 2nd call
extension.create(columnsMock, gridOptionsMock);
extension.register();

expect(mockSelectionModel).toHaveBeenCalledWith(rowSelectionOptionMock);
expect(columnsMock[2]).toEqual(selectionColumn);
expect(columnsMock).toEqual([
{ cssClass: 'red', field: 'field1', id: 'field1', width: 100, },
{ field: 'field2', id: 'field2', width: 50, },
{ excludeFromColumnPicker: true, excludeFromExport: true, excludeFromGridMenu: true, excludeFromHeaderMenu: true, excludeFromQuery: true, field: 'sel', id: '_checkbox_selector', },
]);
});

it('should be able to pre-select rows', (done) => {
Expand All @@ -143,7 +176,7 @@ describe('checkboxSelectorExtension', () => {
setTimeout(() => {
expect(rowSpy).toHaveBeenCalledWith(selectionModelOptions.preselectedRows);
done();
});
}, 0);
});
});
});
9 changes: 8 additions & 1 deletion packages/common/src/extensions/checkboxSelectorExtension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,14 @@ export class CheckboxSelectorExtension implements Extension {
selectionColumn.excludeFromGridMenu = true;
selectionColumn.excludeFromQuery = true;
selectionColumn.excludeFromHeaderMenu = true;
columnDefinitions.unshift(selectionColumn);

// column index position in the grid
const columnPosition = gridOptions?.checkboxSelector?.columnIndexPosition || 0;
if (columnPosition > 0) {
columnDefinitions.splice(columnPosition, 0, selectionColumn);
} else {
columnDefinitions.unshift(selectionColumn);
}
}
return this._addon;
}
Expand Down
265 changes: 265 additions & 0 deletions packages/common/src/filters/__tests__/compoundInputFilter.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,265 @@
import { FieldType, OperatorType } from '../../enums/index';
import { Column, FilterArguments, GridOption } from '../../interfaces/index';
import { Filters } from '../index';
import { CompoundInputFilter } from '../compoundInputFilter';
import { TranslateServiceStub } from '../../../../../test/translateServiceStub';

const containerId = 'demo-container';

// define a <div> container to simulate the grid container
const template = `<div id="${containerId}"></div>`;

const gridOptionMock = {
enableFiltering: true,
} as GridOption;

const gridStub = {
getOptions: () => gridOptionMock,
getColumns: jest.fn(),
getHeaderRowColumn: jest.fn(),
render: jest.fn(),
};

describe('CompoundInputFilter', () => {
let translateService: TranslateServiceStub;
let divContainer: HTMLDivElement;
let filter: CompoundInputFilter;
let filterArguments: FilterArguments;
let spyGetHeaderRow;
let mockColumn: Column;

beforeEach(() => {
translateService = new TranslateServiceStub();

divContainer = document.createElement('div');
divContainer.innerHTML = template;
document.body.appendChild(divContainer);
spyGetHeaderRow = jest.spyOn(gridStub, 'getHeaderRowColumn').mockReturnValue(divContainer);

mockColumn = { id: 'duration', field: 'duration', filterable: true, filter: { model: Filters.input, operator: 'EQ' } };
filterArguments = {
grid: gridStub,
columnDef: mockColumn,
callback: jest.fn()
};

filter = new CompoundInputFilter(translateService);
});

afterEach(() => {
filter.destroy();
});

it('should throw an error when trying to call init without any arguments', () => {
expect(() => filter.init(null)).toThrowError('[Slickgrid-Universal] A filter must always have an "init()" with valid arguments.');
});

it('should initialize the filter', () => {
filter.init(filterArguments);
const filterCount = divContainer.querySelectorAll('.search-filter.filter-duration').length;

expect(spyGetHeaderRow).toHaveBeenCalled();
expect(filterCount).toBe(1);
expect(filter.inputType).toBe('text');
});

it('should have a placeholder when defined in its column definition', () => {
const testValue = 'test placeholder';
mockColumn.filter.placeholder = testValue;

filter.init(filterArguments);
const filterInputElm = divContainer.querySelector<HTMLInputElement>('.search-filter.filter-duration input');

expect(filterInputElm.placeholder).toBe(testValue);
});

it('should call "setValues" and expect that value to be in the callback when triggered', () => {
const spyCallback = jest.spyOn(filterArguments, 'callback');

filter.init(filterArguments);
filter.setValues(['abc']);
const filterInputElm = divContainer.querySelector<HTMLInputElement>('.search-filter.filter-duration input');

filterInputElm.focus();
filterInputElm.dispatchEvent(new (window.window as any).KeyboardEvent('keyup', { keyCode: 97, bubbles: true, cancelable: true }));
const filterFilledElms = divContainer.querySelectorAll<HTMLInputElement>('.search-filter.filter-duration.filled');

expect(filterFilledElms.length).toBe(1);
expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: '', searchTerms: ['abc'], shouldTriggerQuery: true });
});

it('should call "setValues" with "operator" set in the filter arguments and expect that value to be in the callback when triggered', () => {
mockColumn.type = FieldType.number;
const filterArgs = { ...filterArguments, operator: '>' } as FilterArguments;
const spyCallback = jest.spyOn(filterArguments, 'callback');

filter.init(filterArgs);
filter.setValues(['9']);
const filterInputElm = divContainer.querySelector<HTMLInputElement>('.search-filter.filter-duration input');

filterInputElm.focus();
filterInputElm.dispatchEvent(new (window.window as any).KeyboardEvent('keyup', { keyCode: 97, bubbles: true, cancelable: true }));

expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: '>', searchTerms: ['9'], shouldTriggerQuery: true });
});

it('should be able to call "setValues" with a value and an extra operator and expect it to be set as new operator', () => {
mockColumn.type = FieldType.number;
const spyCallback = jest.spyOn(filterArguments, 'callback');

filter.init(filterArguments);
filter.setValues(['9'], OperatorType.greaterThanOrEqual);

const filterSelectElm = divContainer.querySelector<HTMLInputElement>('.search-filter.filter-duration select');
filterSelectElm.dispatchEvent(new CustomEvent('change'));

expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: '>=', searchTerms: ['9'], shouldTriggerQuery: true });
expect(filterSelectElm.value).toBe('>=');
});

it('should trigger an operator change event and expect the callback to be called with the searchTerms and operator defined', () => {
mockColumn.type = FieldType.number;
const spyCallback = jest.spyOn(filterArguments, 'callback');

filter.init(filterArguments);
filter.setValues(['9']);
const filterSelectElm = divContainer.querySelector<HTMLInputElement>('.search-filter.filter-duration select');

filterSelectElm.value = '<=';
filterSelectElm.dispatchEvent(new CustomEvent('change'));

expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: '<=', searchTerms: ['9'], shouldTriggerQuery: true });
});

it('should call "setValues" with extra spaces at the beginning of the searchTerms and trim value when "enableFilterTrimWhiteSpace" is enabled in grid options', () => {
gridOptionMock.enableFilterTrimWhiteSpace = true;
mockColumn.type = FieldType.number;
const filterArgs = { ...filterArguments, operator: '>' } as FilterArguments;
const spyCallback = jest.spyOn(filterArguments, 'callback');

filter.init(filterArgs);
filter.setValues([' 987 ']);
const filterInputElm = divContainer.querySelector<HTMLInputElement>('.search-filter.filter-duration input');

filterInputElm.focus();
filterInputElm.dispatchEvent(new (window.window as any).KeyboardEvent('keyup', { keyCode: 97, bubbles: true, cancelable: true }));

expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: '>', searchTerms: ['987'], shouldTriggerQuery: true });
});

it('should call "setValues" with extra spaces at the beginning of the searchTerms and trim value when "enableTrimWhiteSpace" is enabled in the column filter', () => {
gridOptionMock.enableFilterTrimWhiteSpace = false;
mockColumn.filter.enableTrimWhiteSpace = true;
mockColumn.type = FieldType.number;
const filterArgs = { ...filterArguments, operator: '>' } as FilterArguments;
const spyCallback = jest.spyOn(filterArguments, 'callback');

filter.init(filterArgs);
filter.setValues([' 987 ']);
const filterInputElm = divContainer.querySelector<HTMLInputElement>('.search-filter.filter-duration input');

filterInputElm.focus();
filterInputElm.dispatchEvent(new (window.window as any).KeyboardEvent('keyup', { keyCode: 97, bubbles: true, cancelable: true }));

expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: '>', searchTerms: ['987'], shouldTriggerQuery: true });
});

it('should trigger the callback method when user types something in the input', () => {
const spyCallback = jest.spyOn(filterArguments, 'callback');

filter.init(filterArguments);
const filterInputElm = divContainer.querySelector<HTMLInputElement>('.search-filter.filter-duration input');

filterInputElm.focus();
filterInputElm.value = 'a';
filterInputElm.dispatchEvent(new (window.window as any).KeyboardEvent('keyup', { keyCode: 97, bubbles: true, cancelable: true }));

expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: '', searchTerms: ['a'], shouldTriggerQuery: true });
});

it('should create the input filter with a default search term when passed as a filter argument', () => {
filterArguments.searchTerms = ['xyz'];

filter.init(filterArguments);
const filterInputElm = divContainer.querySelector<HTMLInputElement>('.search-filter.filter-duration input');

expect(filterInputElm.value).toBe('xyz');
});

it('should expect the input not to have the "filled" css class when the search term provided is an empty string', () => {
filterArguments.searchTerms = [''];

filter.init(filterArguments);
const filterInputElm = divContainer.querySelector<HTMLInputElement>('.search-filter.filter-duration input');
const filterFilledElms = divContainer.querySelectorAll<HTMLInputElement>('.search-filter.filter-duration.filled');

expect(filterInputElm.value).toBe('');
expect(filterFilledElms.length).toBe(0);
});

it('should create the input filter with operator dropdown options related to numbers when column definition type is FieldType.number', () => {
mockColumn.type = FieldType.number;
filterArguments.searchTerms = ['9'];

filter.init(filterArguments);
const filterInputElm = divContainer.querySelector<HTMLInputElement>('.search-filter.filter-duration input');
const filterSelectElm = divContainer.querySelectorAll<HTMLSelectElement>('.search-filter.filter-duration select');

expect(filterInputElm.value).toBe('9');
expect(filterSelectElm[0][1].title).toBe('=');
expect(filterSelectElm[0][1].textContent).toBe('=');
expect(filterSelectElm[0][2].textContent).toBe('<');
expect(filterSelectElm[0][3].textContent).toBe('<=');
expect(filterSelectElm[0][4].textContent).toBe('>');
expect(filterSelectElm[0][5].textContent).toBe('>=');
expect(filterSelectElm[0][6].textContent).toBe('<>');
});

it('should create the input filter with operator dropdown options related to strings when column definition type is FieldType.string', () => {
mockColumn.type = FieldType.string;
filterArguments.searchTerms = ['xyz'];

filter.init(filterArguments);
const filterInputElm = divContainer.querySelector<HTMLInputElement>('.search-filter.filter-duration input');
const filterSelectElm = divContainer.querySelectorAll<HTMLSelectElement>('.search-filter.filter-duration select');

expect(filterInputElm.value).toBe('xyz');
expect(filterSelectElm[0][0].title).toBe('Contains');
expect(filterSelectElm[0][1].title).toBe('Equals');
expect(filterSelectElm[0][2].title).toBe('Starts With');
expect(filterSelectElm[0][3].title).toBe('Ends With');
expect(filterSelectElm[0][1].textContent).toBe('=');
expect(filterSelectElm[0][2].textContent).toBe('a*');
expect(filterSelectElm[0][3].textContent).toBe('*z');
});

it('should trigger a callback with the clear filter set when calling the "clear" method', () => {
const spyCallback = jest.spyOn(filterArguments, 'callback');
filterArguments.searchTerms = ['xyz'];

filter.init(filterArguments);
filter.clear();
const filterInputElm = divContainer.querySelector<HTMLInputElement>('.search-filter.filter-duration input');
const filterFilledElms = divContainer.querySelectorAll<HTMLInputElement>('.search-filter.filter-duration.filled');


expect(filterInputElm.value).toBe('');
expect(filterFilledElms.length).toBe(0);
expect(spyCallback).toHaveBeenCalledWith(undefined, { columnDef: mockColumn, clearFilterTriggered: true, shouldTriggerQuery: true });
});

it('should trigger a callback with the clear filter but without querying when when calling the "clear" method with False as argument', () => {
const spyCallback = jest.spyOn(filterArguments, 'callback');
filterArguments.searchTerms = ['xyz'];

filter.init(filterArguments);
filter.clear(false);
const filterInputElm = divContainer.querySelector<HTMLInputElement>('.search-filter.filter-duration input');
const filterFilledElms = divContainer.querySelectorAll<HTMLInputElement>('.search-filter.filter-duration.filled');


expect(filterInputElm.value).toBe('');
expect(filterFilledElms.length).toBe(0);
expect(spyCallback).toHaveBeenCalledWith(undefined, { columnDef: mockColumn, clearFilterTriggered: true, shouldTriggerQuery: false });
});
});
Loading

0 comments on commit 76b4177

Please sign in to comment.