import { AttachmentFileTypeOptions, ColumnType, DateComparisonOptions, FilterCondition, FilterType, IAttachment, MaybeAbsentCellValue, OperatorType } from '../types'; import { exhaustiveSwitchThrow } from '../typeUtils'; import { CONDITION_OPTIONS, DATE_COMPARISON_VALUE, FILTER_TYPE_MAP, } from './constants'; import { AttachmentFilterConditions, CheckboxFilterConditions, DateFilterConditions, IAdaptedFilterCtrl, ITableColumn, MultiSelectFilterConditions, NumberFilterConditions, SelectFilterConditions, StatusFilterConditions, TextFilterConditions, } from './types'; import { BooleanFilter } from '.'; import { formatISO } from 'date-fns'; type TestFilterString = string | string[] | boolean; type FilterSpecificControls = Pick< IAdaptedFilterCtrl, 'attachmentFileTypeOptions' | 'dateComparisonOptions' | 'filterByMe' >; /** Raw data for each unit test created manually */ interface RawTestData { cell: MaybeAbsentCellValue; filStr: TestFilterString; /** Some tests need options specific to certain filter conditions */ options?: FilterSpecificControls; } /** Raw data is organized into 2 categories: those that should pass and those that should fail a filter */ interface RawTestDataByExpectedResult { pass: RawTestData[]; fail: RawTestData[]; } /** Data for each unit test generated from raw data */ interface TestData { cellValue: MaybeAbsentCellValue; model: IAdaptedFilterCtrl; } /** Test data built from the raw data by getTestDataFromRawData */ interface TestDataByFilterResult { pass: TestData[]; fail: TestData[]; } /** * Test data is organized by test condition and expected result before executing * This is so the shell output is easier to read */ type TestDataByCondition = { [key in FilterCondition]?: TestData[]; }; const TEXT_ONE = 'test string'; const TEXT_TWO = 'banana'; const TEXT_FILTER_RAW_TEST_DATA: Record< TextFilterConditions, RawTestDataByExpectedResult > = { [FilterCondition.EQUALS]: { pass: [ { cell: TEXT_ONE, filStr: '' }, { cell: TEXT_ONE, filStr: TEXT_ONE }, ], fail: [{ cell: TEXT_ONE, filStr: TEXT_TWO }], }, [FilterCondition.NOT_EQUAL]: { pass: [{ cell: TEXT_ONE, filStr: TEXT_TWO }], fail: [{ cell: TEXT_ONE, filStr: TEXT_ONE }], }, [FilterCondition.STARTS_WITH]: { pass: [ { cell: TEXT_ONE, filStr: TEXT_ONE }, { cell: TEXT_ONE, filStr: TEXT_ONE.slice(0, 1) }, ], fail: [{ cell: TEXT_ONE, filStr: TEXT_TWO }], }, [FilterCondition.ENDS_WITH]: { pass: [ { cell: TEXT_ONE, filStr: TEXT_ONE }, { cell: TEXT_ONE, filStr: TEXT_ONE.slice(-1) }, ], fail: [{ cell: TEXT_ONE, filStr: TEXT_TWO }], }, [FilterCondition.CONTAINS]: { pass: [ { cell: TEXT_ONE, filStr: TEXT_ONE }, { cell: TEXT_ONE, filStr: TEXT_ONE.slice(1, TEXT_ONE.length - 1), }, ], fail: [{ cell: TEXT_ONE, filStr: TEXT_TWO }], }, [FilterCondition.NOT_CONTAINS]: { pass: [{ cell: TEXT_ONE, filStr: TEXT_TWO }], fail: [ { cell: TEXT_ONE, filStr: TEXT_ONE }, { cell: TEXT_ONE, filStr: TEXT_ONE.slice(1, TEXT_ONE.length - 1), }, ], }, [FilterCondition.IS_EMPTY]: { pass: [{ cell: undefined, filStr: '' }], fail: [{ cell: TEXT_ONE, filStr: '' }], }, [FilterCondition.IS_NOT_EMPTY]: { pass: [{ cell: TEXT_ONE, filStr: '' }], fail: [{ cell: undefined, filStr: '' }], }, }; const NUM = 1; const EQUAL_NUM = String(NUM); const LESSER_NUM = String(NUM - 1); const LARGER_NUM = String(NUM + 1); const NUMBER_FILTER_RAW_TEST_DATA: Record< NumberFilterConditions, RawTestDataByExpectedResult > = { [FilterCondition.EQUALS]: { pass: [{ cell: NUM, filStr: EQUAL_NUM }], fail: [{ cell: NUM, filStr: LARGER_NUM }], }, [FilterCondition.NOT_EQUAL]: { pass: [{ cell: NUM, filStr: LARGER_NUM }], fail: [{ cell: NUM, filStr: EQUAL_NUM }], }, [FilterCondition.LESS_THAN]: { pass: [{ cell: NUM, filStr: LARGER_NUM }], fail: [{ cell: NUM, filStr: EQUAL_NUM }], }, [FilterCondition.LESS_THAN_OR_EQUAL]: { pass: [ { cell: NUM, filStr: EQUAL_NUM }, { cell: NUM, filStr: LARGER_NUM }, ], fail: [{ cell: NUM, filStr: LESSER_NUM }], }, [FilterCondition.GREATER_THAN]: { pass: [{ cell: NUM, filStr: LESSER_NUM }], fail: [{ cell: NUM, filStr: EQUAL_NUM }], }, [FilterCondition.GREATER_THAN_OR_EQUAL]: { pass: [ { cell: NUM, filStr: EQUAL_NUM }, { cell: NUM, filStr: LESSER_NUM }, ], fail: [{ cell: NUM, filStr: LARGER_NUM }], }, [FilterCondition.IS_EMPTY]: { pass: [{ cell: undefined, filStr: '' }], fail: [{ cell: NUM, filStr: '' }], }, [FilterCondition.IS_NOT_EMPTY]: { pass: [{ cell: NUM, filStr: '' }], fail: [{ cell: undefined, filStr: '' }], }, }; const TODAY = formatISO(new Date()); const TOMORROW = DATE_COMPARISON_VALUE[DateComparisonOptions.TOMORROW](); const YESTERDAY = DATE_COMPARISON_VALUE[DateComparisonOptions.YESTERDAY](); const EPOCH = '1970-01-01T00:00:00.000Z'; const CYBERPUNK = '2077-01-01T00:00:00.000Z'; const DATE_FILTER_RAW_TEST_DATA: Record< DateFilterConditions, RawTestDataByExpectedResult > = { [FilterCondition.EQUALS]: { pass: [ { cell: TODAY, filStr: '', options: { dateComparisonOptions: DateComparisonOptions.TODAY }, }, { cell: EPOCH, filStr: EPOCH, options: { dateComparisonOptions: DateComparisonOptions.EXACT_DATE }, }, { cell: YESTERDAY, filStr: '1', options: { dateComparisonOptions: DateComparisonOptions.NUM_DAYS_AGO }, }, { cell: TOMORROW, filStr: '1', options: { dateComparisonOptions: DateComparisonOptions.NUM_DAYS_FROM_NOW, }, }, ], fail: [ { cell: TODAY, filStr: '', options: { dateComparisonOptions: DateComparisonOptions.YESTERDAY }, }, { cell: EPOCH, filStr: CYBERPUNK, options: { dateComparisonOptions: DateComparisonOptions.EXACT_DATE }, }, { cell: TOMORROW, filStr: '1', options: { dateComparisonOptions: DateComparisonOptions.NUM_DAYS_AGO }, }, { cell: YESTERDAY, filStr: '1', options: { dateComparisonOptions: DateComparisonOptions.NUM_DAYS_FROM_NOW, }, }, ], }, [FilterCondition.NOT_EQUAL]: { pass: [ { cell: TODAY, filStr: '', options: { dateComparisonOptions: DateComparisonOptions.YESTERDAY }, }, { cell: EPOCH, filStr: CYBERPUNK, options: { dateComparisonOptions: DateComparisonOptions.EXACT_DATE }, }, { cell: TOMORROW, filStr: '1', options: { dateComparisonOptions: DateComparisonOptions.NUM_DAYS_AGO }, }, { cell: YESTERDAY, filStr: '1', options: { dateComparisonOptions: DateComparisonOptions.NUM_DAYS_FROM_NOW, }, }, ], fail: [ { cell: TODAY, filStr: '', options: { dateComparisonOptions: DateComparisonOptions.TODAY }, }, { cell: EPOCH, filStr: EPOCH, options: { dateComparisonOptions: DateComparisonOptions.EXACT_DATE }, }, { cell: YESTERDAY, filStr: '1', options: { dateComparisonOptions: DateComparisonOptions.NUM_DAYS_AGO }, }, { cell: TOMORROW, filStr: '1', options: { dateComparisonOptions: DateComparisonOptions.NUM_DAYS_FROM_NOW, }, }, ], }, [FilterCondition.LESS_THAN]: { pass: [ { cell: TODAY, filStr: '', options: { dateComparisonOptions: DateComparisonOptions.ONE_WEEK_FROM_NOW, }, }, { cell: EPOCH, filStr: CYBERPUNK, options: { dateComparisonOptions: DateComparisonOptions.EXACT_DATE }, }, { cell: TODAY, filStr: '1', options: { dateComparisonOptions: DateComparisonOptions.NUM_DAYS_FROM_NOW, }, }, ], fail: [ { cell: TODAY, filStr: '', options: { dateComparisonOptions: DateComparisonOptions.TODAY, }, }, { cell: TODAY, filStr: '', options: { dateComparisonOptions: DateComparisonOptions.ONE_WEEK_AGO, }, }, { cell: CYBERPUNK, filStr: EPOCH, options: { dateComparisonOptions: DateComparisonOptions.EXACT_DATE }, }, { cell: TODAY, filStr: '1', options: { dateComparisonOptions: DateComparisonOptions.NUM_DAYS_AGO }, }, ], }, [FilterCondition.LESS_THAN_OR_EQUAL]: { pass: [ { cell: TODAY, filStr: '', options: { dateComparisonOptions: DateComparisonOptions.TODAY, }, }, { cell: TODAY, filStr: '', options: { dateComparisonOptions: DateComparisonOptions.ONE_WEEK_FROM_NOW, }, }, { cell: EPOCH, filStr: CYBERPUNK, options: { dateComparisonOptions: DateComparisonOptions.EXACT_DATE }, }, { cell: TODAY, filStr: '1', options: { dateComparisonOptions: DateComparisonOptions.NUM_DAYS_FROM_NOW, }, }, ], fail: [ { cell: TODAY, filStr: '', options: { dateComparisonOptions: DateComparisonOptions.ONE_WEEK_AGO, }, }, { cell: CYBERPUNK, filStr: EPOCH, options: { dateComparisonOptions: DateComparisonOptions.EXACT_DATE }, }, { cell: TODAY, filStr: '1', options: { dateComparisonOptions: DateComparisonOptions.NUM_DAYS_AGO }, }, ], }, [FilterCondition.GREATER_THAN]: { pass: [ { cell: TODAY, filStr: '', options: { dateComparisonOptions: DateComparisonOptions.ONE_WEEK_AGO, }, }, { cell: CYBERPUNK, filStr: EPOCH, options: { dateComparisonOptions: DateComparisonOptions.EXACT_DATE }, }, { cell: TODAY, filStr: '1', options: { dateComparisonOptions: DateComparisonOptions.NUM_DAYS_AGO }, }, ], fail: [ { cell: TODAY, filStr: '', options: { dateComparisonOptions: DateComparisonOptions.TODAY, }, }, { cell: TODAY, filStr: '', options: { dateComparisonOptions: DateComparisonOptions.ONE_WEEK_FROM_NOW, }, }, { cell: EPOCH, filStr: CYBERPUNK, options: { dateComparisonOptions: DateComparisonOptions.EXACT_DATE }, }, { cell: TODAY, filStr: '1', options: { dateComparisonOptions: DateComparisonOptions.NUM_DAYS_FROM_NOW, }, }, ], }, [FilterCondition.GREATER_THAN_OR_EQUAL]: { pass: [ { cell: TODAY, filStr: '', options: { dateComparisonOptions: DateComparisonOptions.TODAY, }, }, { cell: TODAY, filStr: '', options: { dateComparisonOptions: DateComparisonOptions.ONE_WEEK_AGO, }, }, { cell: CYBERPUNK, filStr: EPOCH, options: { dateComparisonOptions: DateComparisonOptions.EXACT_DATE }, }, { cell: TODAY, filStr: '1', options: { dateComparisonOptions: DateComparisonOptions.NUM_DAYS_AGO }, }, ], fail: [ { cell: TODAY, filStr: '', options: { dateComparisonOptions: DateComparisonOptions.ONE_WEEK_FROM_NOW, }, }, { cell: EPOCH, filStr: CYBERPUNK, options: { dateComparisonOptions: DateComparisonOptions.EXACT_DATE }, }, { cell: TODAY, filStr: '1', options: { dateComparisonOptions: DateComparisonOptions.NUM_DAYS_FROM_NOW, }, }, ], }, [FilterCondition.IS_EMPTY]: { pass: [{ cell: undefined, filStr: '' }], fail: [{ cell: TODAY, filStr: '' }], }, [FilterCondition.IS_NOT_EMPTY]: { pass: [{ cell: TODAY, filStr: '' }], fail: [{ cell: undefined, filStr: '' }], }, [FilterCondition.INTERSECTS]: { pass: [], fail: [], }, }; const SEL_ONE = 'sel1'; const SEL_TWO = 'sel2'; const SEL_THREE = 'sel3'; const SELECT_FILTER_RAW_TEST_DATA: Record< SelectFilterConditions, RawTestDataByExpectedResult > = { [FilterCondition.EQUALS]: { pass: [{ cell: SEL_ONE, filStr: SEL_ONE }], fail: [{ cell: SEL_ONE, filStr: SEL_TWO }], }, [FilterCondition.NOT_EQUAL]: { pass: [{ cell: SEL_ONE, filStr: SEL_TWO }], fail: [{ cell: SEL_ONE, filStr: SEL_ONE }], }, [FilterCondition.EQUALS_ANY_OF]: { pass: [{ cell: SEL_ONE, filStr: [SEL_ONE, SEL_THREE] }], fail: [{ cell: SEL_ONE, filStr: [SEL_TWO, SEL_THREE] }], }, [FilterCondition.EQUALS_NONE_OF]: { pass: [{ cell: SEL_ONE, filStr: [SEL_TWO, SEL_THREE] }], fail: [{ cell: SEL_ONE, filStr: [SEL_ONE, SEL_THREE] }], }, [FilterCondition.IS_EMPTY]: { pass: [{ cell: undefined, filStr: '' }], fail: [{ cell: SEL_ONE, filStr: '' }], }, [FilterCondition.IS_NOT_EMPTY]: { pass: [{ cell: SEL_ONE, filStr: '' }], fail: [{ cell: undefined, filStr: '' }], }, }; const STATUS_FILTER_RAW_TEST_DATA: Record< StatusFilterConditions | MultiSelectFilterConditions, RawTestDataByExpectedResult > = { [FilterCondition.EQUALS]: { pass: [{ cell: SEL_ONE, filStr: SEL_ONE }], fail: [{ cell: SEL_ONE, filStr: SEL_TWO }], }, [FilterCondition.NOT_EQUAL]: { pass: [{ cell: SEL_ONE, filStr: SEL_TWO }], fail: [{ cell: SEL_ONE, filStr: SEL_ONE }], }, [FilterCondition.EQUALS_ANY_OF]: { pass: [{ cell: SEL_ONE, filStr: [SEL_ONE, SEL_THREE] }], fail: [{ cell: SEL_ONE, filStr: [SEL_TWO, SEL_THREE] }], }, [FilterCondition.EQUALS_NONE_OF]: { pass: [{ cell: SEL_ONE, filStr: [SEL_TWO, SEL_THREE] }], fail: [{ cell: SEL_ONE, filStr: [SEL_ONE, SEL_THREE] }], }, [FilterCondition.HAS_ALL_OF]: { pass: [{ cell: [SEL_TWO, SEL_THREE], filStr: [SEL_TWO, SEL_THREE] }], fail: [{ cell: [SEL_TWO, SEL_THREE], filStr: [SEL_ONE, SEL_THREE] }], }, [FilterCondition.HAS_ANY_OF]: { pass: [{ cell: [SEL_TWO], filStr: [SEL_TWO, SEL_THREE] }], fail: [{ cell: [SEL_TWO], filStr: [SEL_ONE, SEL_THREE] }], }, [FilterCondition.HAS_NONE_OF]: { pass: [{ cell: [SEL_ONE], filStr: [SEL_TWO, SEL_THREE] }], fail: [{ cell: [SEL_ONE], filStr: [SEL_ONE, SEL_THREE] }], }, [FilterCondition.IS_EMPTY]: { pass: [{ cell: [], filStr: [SEL_TWO, SEL_THREE] }], fail: [{ cell: [SEL_ONE], filStr: [SEL_ONE, SEL_THREE] }], }, [FilterCondition.IS_NOT_EMPTY]: { pass: [{ cell: [SEL_ONE], filStr: [SEL_TWO, SEL_THREE] }], fail: [{ cell: [], filStr: [SEL_ONE, SEL_THREE] }], }, }; const MULTI_SELECT_FILTER_RAW_TEST_DATA: Record< MultiSelectFilterConditions, RawTestDataByExpectedResult > = { [FilterCondition.EQUALS]: { pass: [{ cell: [SEL_ONE, SEL_TWO], filStr: [SEL_ONE, SEL_TWO] }], fail: [{ cell: [SEL_ONE, SEL_TWO], filStr: [SEL_ONE, SEL_THREE] }], }, [FilterCondition.HAS_ANY_OF]: { pass: [{ cell: [SEL_ONE], filStr: [SEL_ONE, SEL_TWO] }], fail: [{ cell: [SEL_THREE], filStr: [SEL_ONE, SEL_TWO] }], }, [FilterCondition.HAS_ALL_OF]: { pass: [ { cell: [SEL_ONE, SEL_TWO, SEL_THREE], filStr: [SEL_ONE, SEL_TWO], }, ], fail: [ { cell: [SEL_ONE, SEL_TWO], filStr: [SEL_ONE, SEL_TWO, SEL_THREE], }, ], }, [FilterCondition.HAS_NONE_OF]: { pass: [{ cell: [SEL_ONE], filStr: [SEL_TWO, SEL_THREE] }], fail: [{ cell: [SEL_ONE], filStr: [SEL_ONE, SEL_THREE] }], }, [FilterCondition.IS_EMPTY]: { pass: [{ cell: undefined, filStr: [] }], fail: [{ cell: [SEL_ONE], filStr: [] }], }, [FilterCondition.IS_NOT_EMPTY]: { pass: [{ cell: [SEL_ONE], filStr: [] }], fail: [{ cell: undefined, filStr: [] }], }, }; const ME = 'me'; const USER_ONE = 'user1'; const USER_TWO = 'user2'; const COLLABORATOR_FILTER_RAW_TEST_DATA: Record< MultiSelectFilterConditions, RawTestDataByExpectedResult > = { [FilterCondition.EQUALS]: { pass: [ { cell: [USER_ONE, USER_TWO], filStr: [USER_ONE, USER_TWO] }, { cell: [ME], filStr: [], options: { filterByMe: true }, }, ], fail: [ { cell: [USER_ONE, USER_TWO], filStr: [USER_ONE, ME] }, { cell: [USER_ONE], filStr: [], options: { filterByMe: true }, }, ], }, [FilterCondition.HAS_ANY_OF]: { pass: [ { cell: [USER_ONE], filStr: [USER_ONE, USER_TWO] }, { cell: [USER_ONE, ME], filStr: [], options: { filterByMe: true }, }, ], fail: [ { cell: [ME], filStr: [USER_ONE, USER_TWO] }, { cell: [USER_ONE, USER_TWO], filStr: [], options: { filterByMe: true }, }, ], }, [FilterCondition.HAS_ALL_OF]: { pass: [ { cell: [USER_ONE, USER_TWO, ME], filStr: [USER_ONE, USER_TWO], }, { cell: [USER_ONE, USER_TWO, ME], filStr: [USER_ONE, USER_TWO], options: { filterByMe: true }, }, ], fail: [ { cell: [USER_ONE, USER_TWO], filStr: [USER_ONE, USER_TWO, ME], }, { cell: [USER_ONE, USER_TWO], filStr: [USER_ONE, USER_TWO], options: { filterByMe: true }, }, ], }, [FilterCondition.HAS_NONE_OF]: { pass: [ { cell: [USER_ONE], filStr: [USER_TWO, ME] }, { cell: [USER_ONE], filStr: [], options: { filterByMe: true }, }, ], fail: [ { cell: [USER_ONE], filStr: [USER_ONE, ME] }, { cell: [ME], filStr: [], options: { filterByMe: true }, }, ], }, [FilterCondition.IS_EMPTY]: { pass: [ { cell: undefined, filStr: [] }, { cell: undefined, filStr: [], options: { filterByMe: true }, }, ], fail: [{ cell: [USER_ONE], filStr: [] }], }, [FilterCondition.IS_NOT_EMPTY]: { pass: [{ cell: [USER_ONE], filStr: [] }], fail: [ { cell: undefined, filStr: [] }, { cell: undefined, filStr: [], options: { filterByMe: true }, }, ], }, }; const CREATED_BY_FILTER_RAW_TEST_DATA: Record< SelectFilterConditions, RawTestDataByExpectedResult > = { [FilterCondition.EQUALS]: { pass: [ { cell: USER_ONE, filStr: [USER_ONE] }, { cell: ME, filStr: [], options: { filterByMe: true }, }, ], fail: [ { cell: USER_ONE, filStr: [ME] }, { cell: USER_ONE, filStr: [], options: { filterByMe: true }, }, ], }, [FilterCondition.NOT_EQUAL]: { pass: [ { cell: USER_ONE, filStr: [USER_TWO] }, { cell: USER_ONE, filStr: [], options: { filterByMe: true }, }, ], fail: [ { cell: USER_ONE, filStr: [USER_ONE] }, { cell: ME, filStr: [], options: { filterByMe: true }, }, ], }, [FilterCondition.EQUALS_ANY_OF]: { pass: [ { cell: ME, filStr: [USER_ONE, USER_TWO, ME], }, { cell: ME, filStr: [USER_ONE, USER_TWO], options: { filterByMe: true }, }, ], fail: [ { cell: USER_ONE, filStr: [USER_TWO, ME], }, { cell: USER_ONE, filStr: [USER_TWO], options: { filterByMe: true }, }, ], }, [FilterCondition.EQUALS_NONE_OF]: { pass: [ { cell: USER_ONE, filStr: [USER_TWO, ME] }, { cell: USER_ONE, filStr: [USER_TWO], options: { filterByMe: true }, }, ], fail: [ { cell: USER_ONE, filStr: [USER_ONE, USER_TWO, ME] }, { cell: ME, filStr: [USER_ONE, USER_TWO], options: { filterByMe: true }, }, ], }, [FilterCondition.IS_EMPTY]: { pass: [ { cell: undefined, filStr: [] }, { cell: undefined, filStr: [], options: { filterByMe: true }, }, ], fail: [{ cell: USER_ONE, filStr: [] }], }, [FilterCondition.IS_NOT_EMPTY]: { pass: [{ cell: USER_ONE, filStr: [] }], fail: [ { cell: undefined, filStr: [] }, { cell: undefined, filStr: [], options: { filterByMe: true }, }, ], }, }; const FILE_ONE = { fileId: 'file1', fileType: 'image/jpg', fileKey: 'file1', fileName: 'file1.jpg', fileSize: 1024, largeThumbUrl: 'localhost', mediumThumbUrl: 'localhost', smallThumbUrl: 'localhost', url: 'localhost', }; const FILE_TWO = { fileId: 'file2', fileType: 'video/mp4', fileKey: 'file2', fileName: 'file2.mp4', fileSize: 2048, largeThumbUrl: 'localhost', mediumThumbUrl: 'localhost', smallThumbUrl: 'localhost', url: 'localhost', }; const ATTACHMENT_FILTER_RAW_TEST_DATA: Record< AttachmentFilterConditions, RawTestDataByExpectedResult > = { [FilterCondition.FILE_NAME]: { pass: [{ cell: [FILE_ONE], filStr: FILE_ONE.fileName }], fail: [{ cell: [FILE_ONE], filStr: FILE_TWO.fileName }], }, [FilterCondition.FILE_TYPE]: { pass: [ { cell: [FILE_ONE], filStr: '', options: { attachmentFileTypeOptions: AttachmentFileTypeOptions.IMAGE }, }, ], fail: [ { cell: [FILE_ONE], filStr: '', options: { attachmentFileTypeOptions: AttachmentFileTypeOptions.VIDEO }, }, ], }, [FilterCondition.IS_EMPTY]: { pass: [{ cell: undefined, filStr: '' }], fail: [{ cell: [FILE_ONE], filStr: '' }], }, [FilterCondition.IS_NOT_EMPTY]: { pass: [{ cell: [FILE_ONE], filStr: '' }], fail: [{ cell: undefined, filStr: '' }], }, }; const CHECKBOX_FILTER_RAW_TEST_DATA: Record< CheckboxFilterConditions, RawTestDataByExpectedResult > = { [FilterCondition.EQUALS]: { pass: [ { cell: true, filStr: true }, { cell: false, filStr: false }, { cell: null, filStr: false }, { cell: undefined, filStr: false }, ], fail: [{ cell: true, filStr: false }], }, [FilterCondition.CONTAINS]: { pass: [ {cell: [true, false], filStr: true}, {cell: [true, false], filStr: false}, ], fail: [{cell: [false, false], filStr: true}], }, [FilterCondition.EQUALS_EACH_OF]: { pass: [ {cell: [true, true], filStr: true}, {cell: [false, false], filStr: false}, ], fail: [{cell: [true, false], filStr: true}], }, [FilterCondition.IS_EMPTY]: { pass: [ {cell: [], filStr: ""}, ], fail: [{cell: [true, false], filStr: ""}], }, [FilterCondition.IS_NOT_EMPTY]: { pass: [ {cell: [true], filStr: ""}, ], fail: [{cell: [], filStr: ""}], }, }; describe('BooleanFilter', () => { describe('Basic unit tests for every filter type and condition', () => { // Generate a test name so the shell output is more readable const getTestName = (test: TestData, condition: string) => { const { cellValue: rawCellValue, model: { filterString, filterType, filterByMe, attachmentFileTypeOptions, dateComparisonOptions, }, } = test; let cellValue = rawCellValue; // If filter type is attachment, then the cell value is an object // In this case, use the fileName instead when creating the name if (filterType === FilterType.ATTACHMENT && cellValue) { cellValue = (cellValue as unknown as IAttachment[]).map( ({ fileName }) => fileName ); } // Quotes are added to string and array values const formattedCellValue = typeof cellValue === 'string' || (Array.isArray(cellValue) && cellValue.length) ? `"${cellValue}"` : cellValue; let formattedFilterString: TestFilterString = ''; if ( typeof filterString === 'number' || typeof filterString === 'boolean' ) { formattedFilterString = ` ${filterString}`; } if ( (typeof filterString === 'string' && filterString !== '') || (Array.isArray(filterString) && filterString.length) ) { formattedFilterString = ` "${filterString}"`; } let formattedOption: string = ''; // Some tests use case specific filter options // These are noted with an appended description text for clarity if (attachmentFileTypeOptions) { formattedOption = ` is ${attachmentFileTypeOptions}`; } else if (filterByMe) { formattedOption = ' with filterByMe'; } else if (dateComparisonOptions) { formattedOption = ` ${dateComparisonOptions}`; } return `${formattedCellValue} ${condition}${formattedFilterString}${formattedOption}`; }; const executeTests = ( testDataByCondition: TestDataByCondition, tableColumnsById: { [id: string]: ITableColumn }, expectedResult: boolean ) => { for (const [condition, testsData] of Object.entries( testDataByCondition )) { if (!testsData) continue; for (const testData of testsData) { const { cellValue, model } = testData; const testName = getTestName(testData, condition); test(testName, () => { const filter = new BooleanFilter(); filter.setModel([model], tableColumnsById, ME, undefined, false); const result = filter.doesFilterPass({ data: { col1: cellValue }, }); expect(result).toBe(expectedResult); }); } } }; // Take the manually created raw data and create filter models from them // The raw test data refers to objects in the first half of this file const getTestDataFromRawData = ( baseFilterModel: Omit, rawTestData: RawTestDataByExpectedResult ): TestDataByFilterResult => { if (!rawTestData) return { pass: [], fail: [] }; const passConditions = rawTestData.pass.map( ({ cell, filStr, options }: RawTestData) => ({ cellValue: cell, model: { ...baseFilterModel, ...options, filterString: filStr, }, }) ); const failConditions = rawTestData.fail.map( ({ cell, filStr, options }: RawTestData) => ({ cellValue: cell, model: { ...baseFilterModel, ...options, filterString: filStr, }, }) ); return { pass: passConditions, fail: failConditions, }; }; const columnId = 'col1'; const filterId = 'fil1'; const getTestData = ( filterType: FilterType, condition: FilterCondition ) => { const baseFilterModel = { columnId, filterType, filterCondition: condition, id: filterId, operatorType: OperatorType.AND, }; switch (filterType) { case FilterType.TEXT: case FilterType.PHONE: case FilterType.EMAIL: case FilterType.RECORD_REFERENCE: { const rawTestData = TEXT_FILTER_RAW_TEST_DATA[condition as TextFilterConditions]; return getTestDataFromRawData(baseFilterModel, rawTestData); } case FilterType.NUMBER: { const rawTestData = NUMBER_FILTER_RAW_TEST_DATA[condition as NumberFilterConditions]; return getTestDataFromRawData(baseFilterModel, rawTestData); } case FilterType.DATE: { const rawTestData = DATE_FILTER_RAW_TEST_DATA[condition as NumberFilterConditions]; return getTestDataFromRawData(baseFilterModel, rawTestData); } case FilterType.SELECT: { const rawTestData = SELECT_FILTER_RAW_TEST_DATA[condition as SelectFilterConditions]; return getTestDataFromRawData(baseFilterModel, rawTestData); } case FilterType.STATUS: { const rawTestData = STATUS_FILTER_RAW_TEST_DATA[condition as StatusFilterConditions | MultiSelectFilterConditions]; return getTestDataFromRawData(baseFilterModel, rawTestData); } case FilterType.MULTI_SELECT: { const rawTestData = MULTI_SELECT_FILTER_RAW_TEST_DATA[ condition as MultiSelectFilterConditions ]; return getTestDataFromRawData(baseFilterModel, rawTestData); } case FilterType.COLLABORATOR: { const rawTestData = COLLABORATOR_FILTER_RAW_TEST_DATA[ condition as MultiSelectFilterConditions ]; return getTestDataFromRawData(baseFilterModel, rawTestData); } case FilterType.CREATED_BY: { const rawTestData = CREATED_BY_FILTER_RAW_TEST_DATA[ condition as SelectFilterConditions ]; return getTestDataFromRawData(baseFilterModel, rawTestData); } case FilterType.ATTACHMENT: { const rawTestData = ATTACHMENT_FILTER_RAW_TEST_DATA[ condition as AttachmentFilterConditions ]; return getTestDataFromRawData(baseFilterModel, rawTestData); } case FilterType.CHECKBOX: const rawTestData = CHECKBOX_FILTER_RAW_TEST_DATA[ condition as CheckboxFilterConditions ]; return getTestDataFromRawData(baseFilterModel, rawTestData); case FilterType.LOOKUP: { return; // Lookup are using final type to do the test. } default: { return exhaustiveSwitchThrow({ switchValue: filterType }); } } }; // One filter type can be used by multiple column types // This set ensures that each filter type is only tested once const testedFilterTypes = new Set(); for (const type in ColumnType) { // For every column type, get which filter type to use // The filter type tells us which filter conditions to test // Filter conditions refers to comparisons such as EQUALS and NOT_EQUALS const columnType = type as ColumnType; const filterType = FILTER_TYPE_MAP[columnType]; if (testedFilterTypes.has(filterType)) continue; testedFilterTypes.add(filterType); describe(`${filterType} filters:`, () => { const filterConditions = CONDITION_OPTIONS[filterType]; // Tests are organized by expected result and filter condition const testsExpectedToPassFilterByCondition: TestDataByCondition = {}; const testsExpectedToFailFilterByCondition: TestDataByCondition = {}; const tableColumnsById = { [columnId]: { id: columnId, name: columnId, type: columnType, typeOptions: { type: columnType }, } as ITableColumn, }; for (const { value: condition } of filterConditions) { const testData = getTestData(filterType, condition); if (!testData) return; const { pass, fail } = testData; testsExpectedToPassFilterByCondition[condition] = pass; testsExpectedToFailFilterByCondition[condition] = fail; } describe('filter conditions that should PASS:', () => { executeTests( testsExpectedToPassFilterByCondition, tableColumnsById, true ); }); describe('filter conditions that should FAIL:', () => { executeTests( testsExpectedToFailFilterByCondition, tableColumnsById, false ); }); }); } }); describe('Filter model operators', () => { const COL1 = 'col1'; const COL2 = 'col2'; const COL3 = 'col3'; const STR1 = 'str1'; const STR2 = 'str2'; const STR3 = 'str3'; const testColumnType = ColumnType.TEXT as const; const tableColumnsById = { [COL1]: { id: COL1, name: COL1, type: testColumnType, typeOptions: { type: testColumnType }, }, [COL2]: { id: COL2, name: COL2, type: testColumnType, typeOptions: { type: testColumnType }, }, [COL3]: { id: COL3, name: COL3, type: testColumnType, typeOptions: { type: testColumnType }, }, }; test('AND operator', () => { const models: IAdaptedFilterCtrl[] = [ { columnId: COL1, filterString: STR1, filterType: FILTER_TYPE_MAP[testColumnType], filterCondition: FilterCondition.EQUALS, id: 'filter1', operatorType: OperatorType.AND, }, { columnId: COL2, filterString: STR2, filterType: FILTER_TYPE_MAP[testColumnType], filterCondition: FilterCondition.EQUALS, id: 'filter2', operatorType: OperatorType.AND, }, { columnId: COL3, filterString: STR3, filterType: FILTER_TYPE_MAP[testColumnType], filterCondition: FilterCondition.EQUALS, id: 'filter3', operatorType: OperatorType.AND, }, ]; const filter = new BooleanFilter(); filter.setModel(models, tableColumnsById); expect( filter.doesFilterPass({ data: { [COL1]: STR1, [COL2]: STR2, [COL3]: STR3, }, }) ).toBe(true); expect( filter.doesFilterPass({ data: { [COL1]: STR1, [COL2]: STR1, [COL3]: STR3, }, }) ).toBe(false); }); test('OR operator', () => { const models: IAdaptedFilterCtrl[] = [ { columnId: COL1, filterString: STR1, filterType: FILTER_TYPE_MAP[testColumnType], filterCondition: FilterCondition.EQUALS, id: 'filter1', operatorType: OperatorType.OR, }, { columnId: COL2, filterString: STR2, filterType: FILTER_TYPE_MAP[testColumnType], filterCondition: FilterCondition.EQUALS, id: 'filter2', operatorType: OperatorType.OR, }, { columnId: COL3, filterString: STR3, filterType: FILTER_TYPE_MAP[testColumnType], filterCondition: FilterCondition.EQUALS, id: 'filter3', operatorType: OperatorType.OR, }, ]; const filter = new BooleanFilter(); filter.setModel(models, tableColumnsById); expect( filter.doesFilterPass({ data: { [COL1]: STR1, [COL2]: STR2, [COL3]: STR3, }, }) ).toBe(true); expect( filter.doesFilterPass({ data: { [COL1]: STR1, [COL2]: STR1, [COL3]: STR3, }, }) ).toBe(true); expect( filter.doesFilterPass({ data: { [COL1]: STR3, [COL2]: STR1, [COL3]: STR2, }, }) ).toBe(false); }); }); });