From 362928fbf9e5e1fdfcdb84748c38ed9d09acf599 Mon Sep 17 00:00:00 2001 From: emilyhuxng <61912640+emilyhuxng@users.noreply.github.com> Date: Tue, 14 Mar 2023 10:40:07 -0400 Subject: [PATCH] test: Increase code coverage in package jsapi-utils (#1114) Closes #1096 Increase code coverage in package jsapi-utils --- packages/jsapi-utils/src/DateUtils.test.ts | 85 ++ packages/jsapi-utils/src/Formatter.test.ts | 35 + .../jsapi-utils/src/FormatterUtils.test.ts | 57 +- packages/jsapi-utils/src/TableUtils.test.ts | 1268 +++++++++++++++-- packages/jsapi-utils/src/TableUtils.ts | 32 +- .../formatters/BooleanColumnFormatter.test.ts | 20 + .../src/formatters/BooleanColumnFormatter.ts | 2 +- .../formatters/CharColumnFormatter.test.ts | 10 + .../DateTimeColumnFormatter.test.ts | 148 ++ .../formatters/DefaultColumnFormatter.test.ts | 10 + .../formatters/NumberColumnFormatter.test.ts | 64 + .../formatters/StringColumnFormatter.test.ts | 8 + .../formatters/TableColumnFormatter.test.ts | 38 + 13 files changed, 1667 insertions(+), 110 deletions(-) create mode 100644 packages/jsapi-utils/src/formatters/BooleanColumnFormatter.test.ts create mode 100644 packages/jsapi-utils/src/formatters/CharColumnFormatter.test.ts create mode 100644 packages/jsapi-utils/src/formatters/DefaultColumnFormatter.test.ts create mode 100644 packages/jsapi-utils/src/formatters/StringColumnFormatter.test.ts create mode 100644 packages/jsapi-utils/src/formatters/TableColumnFormatter.test.ts diff --git a/packages/jsapi-utils/src/DateUtils.test.ts b/packages/jsapi-utils/src/DateUtils.test.ts index 543a9f36fc..ca96948119 100644 --- a/packages/jsapi-utils/src/DateUtils.test.ts +++ b/packages/jsapi-utils/src/DateUtils.test.ts @@ -173,3 +173,88 @@ describe('dateTimeString parsing tests', () => { testDateTimeStringThrows('2012-04-20 12:13:14.321Overflow'); }); }); + +describe('makeDateWrapper', () => { + it('should use default values if not given arguments', () => { + const expectedDate = new Date(2022, 0, 1, 0, 0, 0, 0); + + expect( + DateUtils.makeDateWrapper('Asia/Dubai', 2022).valueOf() + ).toStrictEqual(expectedDate.valueOf().toString()); + }); +}); + +describe('parseDateValues', () => { + it('should return null if any value is invalid', () => { + expect( + DateUtils.parseDateValues( + 'test', + 'test', + 'test', + 'test', + 'test', + 'test', + 'test' + ) + ).toBe(null); + }); +}); + +describe('parseDateRange', () => { + const MS_PER_DAY = 1000 * 60 * 60 * 24; + + function dateDiffInMillisseconds(a: Date, b: Date) { + // Discard the time and time-zone information. + const utc1 = Date.UTC(a.getFullYear(), a.getMonth(), a.getDate()); + const utc2 = Date.UTC(b.getFullYear(), b.getMonth(), b.getDate()); + + return Math.floor(utc2 - utc1); + } + + it('should throw an error if the text is empty', () => { + expect(() => DateUtils.parseDateRange('', 'America/New_York')).toThrowError( + 'Cannot parse date range from empty string' + ); + }); + + it('should return a range of null values if text is "null"', () => { + expect(DateUtils.parseDateRange('null', 'America/New_York')).toEqual([ + null, + null, + ]); + }); + + it('should return a range from today to tomorrow if text is "today"', () => { + const range = DateUtils.parseDateRange('today', 'America/New_York'); + const start = range[0]; + const end = range[1]; + if (start && end) { + const startDate = start?.asDate(); + const endDate = end?.asDate(); + expect(dateDiffInMillisseconds(startDate, endDate)).toBe(MS_PER_DAY); + } + }); + + it('should return null as the end range if text is "now"', () => { + const range = DateUtils.parseDateRange('now', 'America/New_York'); + expect(range[1]).toBeNull(); + }); + + it('should throw an error if a value in text is invalid', () => { + expect(() => + DateUtils.parseDateRange('9999-99-99', 'America/New_York') + ).toThrowError(/Unable to extract date values from/i); + }); +}); + +describe('getJsDate', () => { + it('returns a date object given that input is a number', () => { + const expectedDate = new Date(10000); + expect(DateUtils.getJsDate(10000)).toEqual(expectedDate); + }); + + it('returns a date object given a DateWrapper', () => { + const dateWrapper = DateUtils.makeDateWrapper('America/New_York', 2022); + expect(DateUtils.getJsDate(dateWrapper)).toEqual(dateWrapper.asDate()); + }); +}); diff --git a/packages/jsapi-utils/src/Formatter.test.ts b/packages/jsapi-utils/src/Formatter.test.ts index b6f92e4138..8ea59a6275 100644 --- a/packages/jsapi-utils/src/Formatter.test.ts +++ b/packages/jsapi-utils/src/Formatter.test.ts @@ -73,6 +73,12 @@ describe('makeColumnFormatMap', () => { formatMap.get(TableUtils.dataType.DECIMAL)?.get(conflictingColumnName) ).toBe(lastFormat); }); + + it('returns an empty map if columnFormattingRules is null', () => { + // @ts-expect-error test null columnFormattingRules + const formatMap = Formatter.makeColumnFormatMap(null); + expect(formatMap.size).toBe(0); + }); }); it('returns correct formatters for given column types', () => { @@ -136,6 +142,11 @@ describe('getColumnFormat', () => { }); describe('getFormattedString', () => { + it('returns an empty string when value is null', () => { + const formatter = makeFormatter(); + expect(formatter.getFormattedString(null, 'decimal')).toBe(''); + }); + it('passes undefined to formatter.format for column with no custom format', () => { const value = 'randomValue'; const columnType = TYPE_DATETIME; @@ -182,3 +193,27 @@ describe('getFormattedString', () => { columnTypeFormatter.format = originalFormatFn; }); }); + +describe('getColumnFormatMapForType', () => { + it('should get columnFormatMap for a given column type and create new map entry', () => { + const formatter = makeFormatter(); + const formatMap = formatter.getColumnFormatMapForType('decimal', true); + if (formatMap) { + expect(formatMap).not.toBeUndefined(); + expect(formatMap.size).toBe(0); + } + }); + + it('returns undefined if no formatmap exists and createIfNecessary is false', () => { + const formatter = new Formatter(); + const formatMap = formatter.getColumnFormatMapForType('decimal'); + expect(formatMap).toBeUndefined(); + }); +}); + +describe('timeZone', () => { + it('should return the time zone name', () => { + const formatter = makeFormatter(); + expect(formatter.timeZone).toBe('America/New_York'); + }); +}); diff --git a/packages/jsapi-utils/src/FormatterUtils.test.ts b/packages/jsapi-utils/src/FormatterUtils.test.ts index 7423789d8b..c8bf9d9555 100644 --- a/packages/jsapi-utils/src/FormatterUtils.test.ts +++ b/packages/jsapi-utils/src/FormatterUtils.test.ts @@ -1,11 +1,15 @@ import Formatter from './Formatter'; -import FormatterUtils from './FormatterUtils'; +import FormatterUtils, { + getColumnFormats, + getDateTimeFormatterOptions, +} from './FormatterUtils'; import { TableColumnFormat, TableColumnFormatter, TableColumnFormatType, } from './formatters'; import TableUtils from './TableUtils'; +import { ColumnFormatSettings, DateTimeFormatSettings } from './Settings'; function makeFormatter(...settings: ConstructorParameters) { return new Formatter(...settings); @@ -83,3 +87,54 @@ describe('isCustomColumnFormatDefined', () => { ).toBe(false); }); }); + +describe('getColumnFormats', () => { + it('should return an array of format rules', () => { + const settings: ColumnFormatSettings = { + formatter: [ + { + columnType: 'integer', + columnName: 'test1', + format: { + label: 'test1', + formatString: '0.0', + type: 'type-context-custom', + }, + }, + { + columnType: 'decimal', + columnName: 'test2', + format: { + label: 'test2', + formatString: '0.0', + type: 'type-context-custom', + }, + }, + ], + }; + + expect(getColumnFormats(settings)).toEqual(settings.formatter); + }); + + it('should return undefined if settings or settings.formatter is undefined', () => { + expect(getColumnFormats()).toBeUndefined(); + }); +}); + +describe('getDateTimeFormatterOptions', () => { + it('should return an object containing date and time formatter options', () => { + const settings: DateTimeFormatSettings = { + timeZone: 'America/New_York', + defaultDateTimeFormat: 'yyyy-MM-dd HH:mm:ss.SSS', + showTimeZone: true, + showTSeparator: false, + }; + const expectedObject = { + ...settings, + defaultDateTimeFormatString: 'yyyy-MM-dd HH:mm:ss.SSS', + }; + delete expectedObject.defaultDateTimeFormat; + + expect(getDateTimeFormatterOptions(settings)).toEqual(expectedObject); + }); +}); diff --git a/packages/jsapi-utils/src/TableUtils.test.ts b/packages/jsapi-utils/src/TableUtils.test.ts index e2536e02b7..f08f500543 100644 --- a/packages/jsapi-utils/src/TableUtils.test.ts +++ b/packages/jsapi-utils/src/TableUtils.test.ts @@ -1,16 +1,19 @@ import dh, { Column, DateWrapper, + FilterCondition, FilterValue, + LongWrapper, Sort, Table, + TreeTable, } from '@deephaven/jsapi-shim'; import { Operator as FilterOperator, Type as FilterType, TypeValue as FilterTypeValue, } from '@deephaven/filters'; -import TableUtils from './TableUtils'; +import TableUtils, { DataType, SortDirection } from './TableUtils'; import DateUtils from './DateUtils'; // eslint-disable-next-line import/no-relative-packages import IrisGridTestUtils from '../../iris-grid/src/IrisGridTestUtils'; @@ -33,67 +36,75 @@ function makeColumns(count = 5): Column[] { return columns; } -it('toggles sort properly', () => { - const columns = makeColumns(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const table: Table = new (dh as any).Table({ columns }); - let tableSorts: Sort[] = []; - - expect(table).not.toBe(null); - expect(table.sort.length).toBe(0); - - tableSorts = TableUtils.toggleSortForColumn(tableSorts, columns, 0, true); - table.applySort(tableSorts); - expect(table.sort.length).toBe(1); - expect(table.sort[0].column).toBe(columns[0]); - expect(table.sort[0].direction).toBe(TableUtils.sortDirection.ascending); - - tableSorts = TableUtils.toggleSortForColumn(tableSorts, columns, 3, true); - table.applySort(tableSorts); - expect(table.sort.length).toBe(2); - expect(table.sort[0].column).toBe(columns[0]); - expect(table.sort[0].direction).toBe(TableUtils.sortDirection.ascending); - expect(table.sort[1].column).toBe(columns[3]); - expect(table.sort[1].direction).toBe(TableUtils.sortDirection.ascending); - - tableSorts = TableUtils.toggleSortForColumn(tableSorts, columns, 0, true); - table.applySort(tableSorts); - expect(table.sort.length).toBe(2); - expect(table.sort[0].column).toBe(columns[3]); - expect(table.sort[0].direction).toBe(TableUtils.sortDirection.ascending); - expect(table.sort[1].column).toBe(columns[0]); - expect(table.sort[1].direction).toBe(TableUtils.sortDirection.descending); - - tableSorts = TableUtils.toggleSortForColumn(tableSorts, columns, 0, true); - table.applySort(tableSorts); - expect(table.sort.length).toBe(1); - expect(table.sort[0].column).toBe(columns[3]); - expect(table.sort[0].direction).toBe(TableUtils.sortDirection.ascending); - - tableSorts = TableUtils.toggleSortForColumn(tableSorts, columns, 3, true); - table.applySort(tableSorts); - expect(table.sort.length).toBe(1); - expect(table.sort[0].column).toBe(columns[3]); - expect(table.sort[0].direction).toBe(TableUtils.sortDirection.descending); - - tableSorts = TableUtils.toggleSortForColumn(tableSorts, columns, 3, true); - table.applySort(tableSorts); - - expect(table.sort.length).toBe(0); -}); +type MockFilterCondition = Record<'not' | 'and' | 'or', jest.Mock>; -describe('quick filter tests', () => { - type MockFilterCondition = Record<'not' | 'and' | 'or', jest.Mock>; +function makeFilterCondition(type = ''): MockFilterCondition { + return { + not: jest.fn(() => makeFilterCondition(`${type}.${FilterType.eq}`)), + and: jest.fn(() => makeFilterCondition(`${type}.${FilterType.eq}`)), + or: jest.fn(() => makeFilterCondition(`${type}.${FilterType.eq}`)), + }; +} - function makeFilterCondition(type = ''): MockFilterCondition { - return { - not: jest.fn(() => makeFilterCondition(`${type}.${FilterType.eq}`)), - and: jest.fn(() => makeFilterCondition(`${type}.${FilterType.eq}`)), - or: jest.fn(() => makeFilterCondition(`${type}.${FilterType.eq}`)), - }; - } +describe('toggleSortForColumn', () => { + it('toggles sort properly', () => { + const columns = makeColumns(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const table: Table = new (dh as any).Table({ columns }); + let tableSorts: Sort[] = []; + + expect(table).not.toBe(null); + expect(table.sort.length).toBe(0); + + tableSorts = TableUtils.toggleSortForColumn(tableSorts, columns, 0, true); + table.applySort(tableSorts); + expect(table.sort.length).toBe(1); + expect(table.sort[0].column).toBe(columns[0]); + expect(table.sort[0].direction).toBe(TableUtils.sortDirection.ascending); + + tableSorts = TableUtils.toggleSortForColumn(tableSorts, columns, 3, true); + table.applySort(tableSorts); + expect(table.sort.length).toBe(2); + expect(table.sort[0].column).toBe(columns[0]); + expect(table.sort[0].direction).toBe(TableUtils.sortDirection.ascending); + expect(table.sort[1].column).toBe(columns[3]); + expect(table.sort[1].direction).toBe(TableUtils.sortDirection.ascending); + + tableSorts = TableUtils.toggleSortForColumn(tableSorts, columns, 0, true); + table.applySort(tableSorts); + expect(table.sort.length).toBe(2); + expect(table.sort[0].column).toBe(columns[3]); + expect(table.sort[0].direction).toBe(TableUtils.sortDirection.ascending); + expect(table.sort[1].column).toBe(columns[0]); + expect(table.sort[1].direction).toBe(TableUtils.sortDirection.descending); + + tableSorts = TableUtils.toggleSortForColumn(tableSorts, columns, 0, true); + table.applySort(tableSorts); + expect(table.sort.length).toBe(1); + expect(table.sort[0].column).toBe(columns[3]); + expect(table.sort[0].direction).toBe(TableUtils.sortDirection.ascending); + + tableSorts = TableUtils.toggleSortForColumn(tableSorts, columns, 3, true); + table.applySort(tableSorts); + expect(table.sort.length).toBe(1); + expect(table.sort[0].column).toBe(columns[3]); + expect(table.sort[0].direction).toBe(TableUtils.sortDirection.descending); + + tableSorts = TableUtils.toggleSortForColumn(tableSorts, columns, 3, true); + table.applySort(tableSorts); + + expect(table.sort.length).toBe(0); + }); + it('should return an empty array if columnIndex is out of range', () => { + const columns = makeColumns(); + expect(TableUtils.toggleSortForColumn([], columns, -1)).toEqual([]); + }); +}); + +describe('quick filter tests', () => { type MockFilter = ReturnType; + type MockColumn = Omit & { filter(): MockFilter }; function makeFilter(type = '') { return { @@ -144,11 +155,11 @@ describe('quick filter tests', () => { }; } - function makeFilterColumn(type = 'string'): Column { + function makeFilterColumn(type = 'string'): MockColumn { const filter = makeFilter(); const column = IrisGridTestUtils.makeColumn('test placeholder', type, 13); column.filter = jest.fn(() => filter); - return column; + return column as MockColumn; } function mockFilterConditionReturnValue(filterToMock): MockFilterCondition { @@ -167,11 +178,27 @@ describe('quick filter tests', () => { const result = TableUtils[functionName](column, text); - if (args.length > 0) { - expect(filter[expectedFn]).toHaveBeenCalledWith(...args); - } else { - expect(filter[expectedFn]).toHaveBeenCalled(); - } + expect(filter[expectedFn]).toHaveBeenCalledWith(...args); + expect(result).toBe(expectedResult); + } + + function testFilterWithType( + functionName: string, + text, + expectedFn, + type, + ...args + ) { + const column = makeFilterColumn(type); + const filter = column.filter(); + + const expectedResult = makeFilterCondition(); + + filter[expectedFn].mockReturnValueOnce(expectedResult); + + const result = TableUtils[functionName](column, text); + + expect(filter[expectedFn]).toHaveBeenCalledWith(...args); expect(result).toBe(expectedResult); } @@ -267,6 +294,380 @@ describe('quick filter tests', () => { (Date.parse(dateString) as unknown) as DateWrapper; }); + describe('makeQuickFilterFromComponent', () => { + const testComponentFilter = ( + text: string | boolean | number, + expectedFn, + type, + ...args + ) => { + testFilterWithType( + 'makeQuickFilterFromComponent', + text, + expectedFn, + type, + ...args + ); + }; + + it('should return a number filter if column type is number', () => { + testComponentFilter('52', FilterType.eq, 'int', 52); + testComponentFilter('>-9', FilterType.greaterThan, 'short', -9); + }); + + it('should return a boolean filter if column type is boolean', () => { + testComponentFilter('true', FilterType.isTrue, 'boolean'); + testComponentFilter(false, FilterType.isFalse, 'boolean'); + testComponentFilter(1, FilterType.isTrue, 'boolean'); + testComponentFilter('null', FilterType.isNull, 'java.lang.Boolean'); + }); + + it('should return a date filter if column type is date', () => { + testMultiFilter( + 'io.deephaven.time.DateTime', + 'makeQuickFilterFromComponent', + '>2018', + 'America/New_York', + [[FilterType.greaterThanOrEqualTo, null, new Date(2019, 0).getTime()]] + ); + testMultiFilter( + 'io.deephaven.db.tables.utils.DBDateTime', + 'makeQuickFilterFromComponent', + '2018-9-7', + 'America/New_York', + [ + [ + FilterType.greaterThanOrEqualTo, + FilterOperator.and, + new Date(2018, 8, 7).getTime(), + ], + [FilterType.lessThan, null, new Date(2018, 8, 8).getTime()], + ] + ); + }); + + it('should return a char filter if column type is char', () => { + testComponentFilter('d', FilterType.eq, 'char', 'd'); + testComponentFilter('!c', FilterType.notEq, 'java.lang.Character', 'c'); + testComponentFilter( + '>=c', + FilterType.greaterThanOrEqualTo, + 'char', + '"c"' + ); + testComponentFilter('null', FilterType.isNull, 'char'); + }); + + it('should return a text filter for any other column type', () => { + testComponentFilter( + '\\*foo', + FilterType.eqIgnoreCase, + 'notatype', + '*foo' + ); + testComponentFilter('foo', FilterType.eqIgnoreCase, 'string', 'foo'); + }); + }); + + describe('makeAdvancedValueFilter', () => { + const filterTypesWithArgument: FilterTypeValue[] = [ + FilterType.eq, + FilterType.eqIgnoreCase, + FilterType.notEq, + FilterType.notEqIgnoreCase, + FilterType.greaterThan, + FilterType.greaterThanOrEqualTo, + FilterType.lessThan, + FilterType.lessThanOrEqualTo, + ]; + + const filterTypesWithNoArguments: FilterTypeValue[] = [ + FilterType.isTrue, + FilterType.isFalse, + FilterType.isNull, + ]; + + const invalidFilterTypes: FilterTypeValue[] = [ + FilterType.in, + FilterType.inIgnoreCase, + FilterType.notIn, + FilterType.notInIgnoreCase, + FilterType.invoke, + ]; + + function testInvokeFilter(type, operation, value, timezone, ...args) { + const column = makeFilterColumn(type); + const filter = column.filter() as MockFilter; + + const nullResult = makeFilterCondition(); + const notResult = makeFilterCondition(); + const invokeResult = makeFilterCondition(); + const expectedResult = makeFilterCondition(); + + filter.isNull.mockReturnValueOnce(nullResult); + + (nullResult.not as jest.Mock).mockReturnValueOnce(notResult); + + filter.invoke.mockReturnValueOnce(invokeResult); + + (notResult.and as jest.Mock).mockReturnValueOnce(expectedResult); + + const result = TableUtils.makeAdvancedValueFilter( + column, + operation, + value, + timezone + ); + + expect(filter.isNull).toHaveBeenCalled(); + expect(nullResult.not).toHaveBeenCalled(); + expect(notResult.and).toHaveBeenCalledWith(invokeResult); + + expect(filter.invoke).toHaveBeenCalledWith(...args); + expect(result).toBe(expectedResult); + } + + const testFilterWithOperation = ( + functionName, + value, + expectedFn, + operation: FilterTypeValue, + timezone: string, + type: string, + ...args + ) => { + const column = makeFilterColumn(type); + const filter = column.filter(); + + const expectedResult = makeFilterCondition(); + + filter[expectedFn].mockReturnValueOnce(expectedResult); + + const result = TableUtils[functionName]( + column, + operation, + value, + timezone + ); + + expect(filter[expectedFn]).toHaveBeenCalledWith(...args); + expect(result).toBe(expectedResult); + }; + + const testAdvancedValueFilter = ( + value, + expectedFn, + operation: FilterTypeValue, + timezone: string, + type: string, + ...args + ) => { + testFilterWithOperation( + 'makeAdvancedValueFilter', + value, + expectedFn, + operation, + timezone, + type, + ...args + ); + }; + + it('should return a date filter if column type is date', () => { + const [startValue] = DateUtils.parseDateRange( + '2022-11-12', + DEFAULT_TIME_ZONE_ID + ); + testAdvancedValueFilter( + '2022-11-12', + FilterType.greaterThanOrEqualTo, + 'greaterThanOrEqualTo', + DEFAULT_TIME_ZONE_ID, + 'io.deephaven.time.DateTime', + startValue + ); + }); + + it('should return a number filter if column type is number', () => { + testAdvancedValueFilter( + '200', + FilterType.eq, + FilterType.eq, + DEFAULT_TIME_ZONE_ID, + 'int', + 200 + ); + }); + + it('should return a char filter if column type is char', () => { + testAdvancedValueFilter( + 'c', + FilterType.notEq, + FilterType.notEq, + DEFAULT_TIME_ZONE_ID, + 'char', + 'c' + ); + }); + + describe('column type is not date, number, or char', () => { + it('should call eq function with the value if operation is "eq"', () => { + testAdvancedValueFilter( + 'test', + FilterType.eq, + FilterType.eq, + DEFAULT_TIME_ZONE_ID, + 'java.lang.String', + 'test' + ); + }); + + it('should call eqIgnoreCase function with the value if operation is "eqIgnoreCase"', () => { + testAdvancedValueFilter( + 'test', + FilterType.eqIgnoreCase, + FilterType.eqIgnoreCase, + DEFAULT_TIME_ZONE_ID, + 'java.lang.String', + 'test' + ); + }); + + it('should call notEq function with the value if operation is "notEq"', () => { + testAdvancedValueFilter( + 'true', + FilterType.notEq, + FilterType.notEq, + DEFAULT_TIME_ZONE_ID, + 'boolean', + 'true' + ); + }); + + it('should call notEqIgnoreCase function with the value if operation is "notEq"', () => { + testAdvancedValueFilter( + 'true', + FilterType.notEq, + FilterType.notEq, + DEFAULT_TIME_ZONE_ID, + 'boolean', + 'true' + ); + }); + + it('handles filter types with argument of value (eq, eqIgnoreCase, notEq, notEqIgnoreCase, greaterThan, greaterThanOrEqualTo, lessThan, lessThanOrEqualTo)', () => { + // eslint-disable-next-line no-restricted-syntax + for (const value of filterTypesWithArgument) { + testAdvancedValueFilter( + 'test', + value, + value, + DEFAULT_TIME_ZONE_ID, + 'java.lang.String', + 'test' + ); + } + }); + + it('handles filter types with no arguments (isTrue, isFalse, isNull)', () => { + // eslint-disable-next-line no-restricted-syntax + for (const value of filterTypesWithNoArguments) { + testAdvancedValueFilter( + 'test', + value, + value, + DEFAULT_TIME_ZONE_ID, + 'java.lang.String' + ); + } + }); + + it('handles contains', () => { + testInvokeFilter( + 'java.lang.String', + FilterType.contains, + 'test', + DEFAULT_TIME_ZONE_ID, + 'matches', + `(?s)(?i).*\\Qtest\\E.*` + ); + }); + + it('handles notContains', () => { + const column = makeFilterColumn('java.lang.String'); + const filter = column.filter() as MockFilter; + + const nullResult = makeFilterCondition(); + const notResult = makeFilterColumn(); + const invokeResult = makeFilterCondition(); + const expectedResult = makeFilterCondition(); + + filter.isNull.mockReturnValueOnce(nullResult); + + filter.invoke.mockReturnValueOnce(invokeResult); + + invokeResult.not.mockReturnValueOnce(notResult); + + nullResult.or.mockReturnValueOnce(expectedResult); + + const result = TableUtils.makeAdvancedValueFilter( + column, + FilterType.notContains, + 'test', + DEFAULT_TIME_ZONE_ID + ); + + expect(filter.isNull).toHaveBeenCalled(); + expect(nullResult.or).toHaveBeenCalledWith(notResult); + + expect(filter.invoke).toHaveBeenCalledWith( + 'matches', + `(?s)(?i).*\\Qtest\\E.*` + ); + + expect(result).toBe(expectedResult); + }); + + it('handles startsWith', () => { + testInvokeFilter( + 'java.lang.String', + FilterType.startsWith, + 'test', + DEFAULT_TIME_ZONE_ID, + 'matches', + `(?s)(?i)^\\Qtest\\E.*` + ); + }); + + it('handles endsWith', () => { + testInvokeFilter( + 'java.lang.String', + FilterType.endsWith, + 'test', + DEFAULT_TIME_ZONE_ID, + 'matches', + `(?s)(?i).*\\Qtest\\E$` + ); + }); + + it('should throw an error for unexpected filter operations', () => { + const column = makeFilterColumn('java.lang.String'); + + // eslint-disable-next-line no-restricted-syntax + for (const operation of invalidFilterTypes) { + expect(() => + TableUtils.makeAdvancedValueFilter( + column, + operation, + 'test', + DEFAULT_TIME_ZONE_ID + ) + ).toThrowError(`Unexpected filter operation: ${operation}`); + } + }); + }); + }); + describe('quick number filters', () => { function testNumberFilter(text, expectedFn, ...args) { testFilter('makeQuickNumberFilter', text, expectedFn, ...args); @@ -279,8 +680,6 @@ describe('quick filter tests', () => { it('handles empty cases', () => { const column = makeFilterColumn(); - expect(TableUtils.makeQuickNumberFilter(column, null)).toBe(null); - expect(TableUtils.makeQuickNumberFilter(column, undefined)).toBe(null); expect(TableUtils.makeQuickNumberFilter(column, '')).toBe(null); }); @@ -487,6 +886,11 @@ describe('quick filter tests', () => { TableUtils.makeQuickNumberFilter(column, '!= null'); expect(expectFilterCondition.not).toHaveBeenCalledTimes(1); }); + + it('should return null if it is an abnormal value with unsupported operations', () => { + const column = makeFilterColumn(); + expect(TableUtils.makeQuickNumberFilter(column, '>=NaN')).toBeNull(); + }); }); describe('quick boolean filters', () => { @@ -507,8 +911,6 @@ describe('quick filter tests', () => { it('handles empty cases', () => { const column = makeFilterColumn(); - expect(TableUtils.makeQuickBooleanFilter(column, null)).toBe(null); - expect(TableUtils.makeQuickBooleanFilter(column, undefined)).toBe(null); expect(TableUtils.makeQuickBooleanFilter(column, '')).toBe(null); }); @@ -615,10 +1017,6 @@ describe('quick filter tests', () => { it('handles invalid cases', () => { const column = makeFilterColumn(); - expect(() => TableUtils.makeQuickDateFilter(column, null, '')).toThrow(); - expect(() => - TableUtils.makeQuickDateFilter(column, undefined, '') - ).toThrow(); expect(() => TableUtils.makeQuickDateFilter(column, '', '')).toThrow(); expect(() => TableUtils.makeQuickDateFilter(column, '>', '')).toThrow(); @@ -1180,19 +1578,13 @@ describe('quick filter tests', () => { expect(nullResult.not).toHaveBeenCalled(); expect(notResult.and).toHaveBeenCalledWith(invokeResult); - if (args.length > 0) { - expect(filter.invoke).toHaveBeenCalledWith(...args); - } else { - expect(filter.invoke).toHaveBeenCalled(); - } + expect(filter.invoke).toHaveBeenCalledWith(...args); expect(result).toBe(expectedResult); } it('handles empty cases', () => { const column = makeFilterColumn(); - expect(TableUtils.makeQuickTextFilter(column, null)).toBe(null); - expect(TableUtils.makeQuickTextFilter(column, undefined)).toBe(null); expect(TableUtils.makeQuickTextFilter(column, '')).toBe(null); }); @@ -1296,6 +1688,13 @@ describe('quick filter tests', () => { ] ); }); + + it('throws an error if filter for andComponent is null', () => { + const column = makeFilterColumn('char'); + expect(() => + TableUtils.makeQuickFilter(column, '12a && 13 || 12') + ).toThrowError('Unable to parse quick filter from text 12a && 13 || 12'); + }); }); describe('quick char filters', () => { @@ -1310,8 +1709,6 @@ describe('quick filter tests', () => { it('handles empty cases', () => { const column = makeFilterColumn(); - expect(TableUtils.makeQuickCharFilter(column, null)).toBe(null); - expect(TableUtils.makeQuickCharFilter(column, undefined)).toBe(null); expect(TableUtils.makeQuickCharFilter(column, '')).toBe(null); }); @@ -1354,6 +1751,134 @@ describe('quick filter tests', () => { testCharFilter('null', FilterType.isNull); }); }); + + describe('makeSelectValueFilter', () => { + const testNoSelectedItems = (type: string, expectedValue: unknown) => { + const column = makeFilterColumn(type); + const filter = column.filter(); + + const eqResult = makeFilterCondition(); + const notEqResult = makeFilterCondition(); + const expectedResult = makeFilterCondition(); + + filter.eq.mockReturnValueOnce(eqResult); + filter.notEq.mockReturnValueOnce(notEqResult); + eqResult.and.mockReturnValueOnce(expectedResult); + + expect(TableUtils.makeSelectValueFilter(column, [], false)).toBe( + expectedResult + ); + expect(filter.eq).toHaveBeenCalledWith(expectedValue); + expect(filter.notEq).toHaveBeenCalledWith(expectedValue); + expect(eqResult.and).toHaveBeenCalledWith(notEqResult); + }; + + it('should return null if there are no selected values and invertSelection is true', () => { + const column = makeFilterColumn(); + + expect(TableUtils.makeSelectValueFilter(column, [], true)).toBeNull(); + }); + + it('handles different column types when there are no selected values and invertSelection is false', () => { + testNoSelectedItems('java.lang.String', 'a'); + testNoSelectedItems('boolean', true); + testNoSelectedItems( + 'io.deephaven.db.tables.utils.DBDateTime', + expect.any(Number) + ); + testNoSelectedItems('int', 0); + }); + + it('handles non-empty selected values with null values and invertSelection is true', () => { + const column = makeFilterColumn('java.lang.String'); + const filter = column.filter(); + + const isNullResult = makeFilterCondition(); + const notResult = makeFilterCondition(); + const notInResult = makeFilterCondition(); + const expectedResult = makeFilterCondition(); + + filter.notIn.mockReturnValueOnce(notInResult); + filter.isNull.mockReturnValueOnce(isNullResult); + isNullResult.not.mockReturnValueOnce(notResult); + notResult.and.mockReturnValueOnce(expectedResult); + expect( + TableUtils.makeSelectValueFilter(column, [null, 'string'], true) + ).toBe(expectedResult); + expect(filter.notIn).toHaveBeenCalledWith(['string']); + expect(notResult.and).toHaveBeenCalledWith(notInResult); + }); + + it('handles non-empty selected values with null values and invertSelection is false', () => { + const column = makeFilterColumn('boolean'); + const filter = column.filter(); + + const isNullResult = makeFilterCondition(); + const inResult = makeFilterCondition(); + const expectedResult = makeFilterCondition(); + + filter.isNull.mockReturnValueOnce(isNullResult); + filter.in.mockReturnValueOnce(inResult); + isNullResult.or.mockReturnValueOnce(expectedResult); + expect( + TableUtils.makeSelectValueFilter(column, [null, true, false], false) + ).toBe(expectedResult); + expect(filter.in).toHaveBeenCalledWith([true, false]); + expect(isNullResult.or).toHaveBeenCalledWith(inResult); + }); + + it('handles all null values as selected values and invertSelection true', () => { + const column = makeFilterColumn('boolean'); + const filter = column.filter(); + + const isNullResult = makeFilterCondition(); + const expectedResult = makeFilterCondition(); + + filter.isNull.mockReturnValueOnce(isNullResult); + isNullResult.not.mockReturnValueOnce(expectedResult); + expect( + TableUtils.makeSelectValueFilter(column, [null, null, null], true) + ).toBe(expectedResult); + }); + + it('handles all null values as selected values and invertSelection false', () => { + const column = makeFilterColumn('boolean'); + const filter = column.filter(); + + const isNullResult = makeFilterCondition(); + + filter.isNull.mockReturnValueOnce(isNullResult); + expect( + TableUtils.makeSelectValueFilter(column, [null, null, null], false) + ).toBe(isNullResult); + }); + + it('handles non-empty selected values but no null values and invertSelection is true', () => { + const column = makeFilterColumn('int'); + const filter = column.filter(); + + const expectedResult = makeFilterCondition(); + + filter.notIn.mockReturnValueOnce(expectedResult); + expect(TableUtils.makeSelectValueFilter(column, [1, 2, 3], true)).toBe( + expectedResult + ); + expect(filter.notIn).toHaveBeenCalledWith([1, 2, 3]); + }); + + it('handles non-empty selected values but no null values and invertSelection is false', () => { + const column = makeFilterColumn('int'); + const filter = column.filter(); + + const expectedResult = makeFilterCondition(); + + filter.in.mockReturnValueOnce(expectedResult); + expect(TableUtils.makeSelectValueFilter(column, [1, 2, 3], false)).toBe( + expectedResult + ); + expect(filter.in).toHaveBeenCalledWith([1, 2, 3]); + }); + }); }); describe('makeCancelableTableEventPromise', () => { @@ -1535,3 +2060,582 @@ describe('Sorting', () => { ); }); }); + +describe('isTreeTable', () => { + it('should return true if table is a TreeTable', () => { + const table: TreeTable = ({ + expand: jest.fn(), + collapse: jest.fn(), + } as unknown) as TreeTable; + + expect(TableUtils.isTreeTable(table)).toBe(true); + }); + + it('should return false if table is not a TreeTable', () => { + const table = null; + expect(TableUtils.isTreeTable(table)).toBe(false); + }); +}); + +describe('makeNumberValue', () => { + it('should return null if text is "null" or ""', () => { + expect(TableUtils.makeNumberValue('null')).toBeNull(); + expect(TableUtils.makeNumberValue('')).toBeNull(); + }); + + it('should return positive infinity if text is ∞, infinity, or inf', () => { + expect(TableUtils.makeNumberValue('∞')).toBe(Number.POSITIVE_INFINITY); + expect(TableUtils.makeNumberValue('infinity')).toBe( + Number.POSITIVE_INFINITY + ); + expect(TableUtils.makeNumberValue('inf')).toBe(Number.POSITIVE_INFINITY); + }); + + it('should return positive infinity if text is -∞, -infinity, or -inf', () => { + expect(TableUtils.makeNumberValue('-∞')).toBe(Number.NEGATIVE_INFINITY); + expect(TableUtils.makeNumberValue('-infinity')).toBe( + Number.NEGATIVE_INFINITY + ); + expect(TableUtils.makeNumberValue('-inf')).toBe(Number.NEGATIVE_INFINITY); + }); + + it('should return a number if text is a number', () => { + expect(TableUtils.makeNumberValue('123')).toBe(123); + }); + + it('should throw an error if the text is not a number of any of the special values', () => { + expect(() => TableUtils.makeNumberValue('test')).toThrowError( + `Invalid number 'test'` + ); + }); +}); + +describe('makeBooleanValue', () => { + const testMakeBooleanValue = ( + text: string, + expectedValue: boolean | null, + allowEmpty = false + ) => { + expect(TableUtils.makeBooleanValue(text, allowEmpty)).toBe(expectedValue); + }; + + it('should return null if text is empty and allowEmpty is true', () => { + testMakeBooleanValue('', null, true); + }); + + it('should return null if text is "null"', () => { + testMakeBooleanValue('null', null); + }); + + it('shouhld return false for different variations of false', () => { + testMakeBooleanValue('0', false); + testMakeBooleanValue('f', false); + testMakeBooleanValue('fa', false); + testMakeBooleanValue('fal', false); + testMakeBooleanValue('fals', false); + testMakeBooleanValue('false', false); + testMakeBooleanValue('n', false); + testMakeBooleanValue('no', false); + }); + + it('should return false for different variations of false', () => { + testMakeBooleanValue('1', true); + testMakeBooleanValue('t', true); + testMakeBooleanValue('tr', true); + testMakeBooleanValue('tru', true); + testMakeBooleanValue('true', true); + testMakeBooleanValue('y', true); + testMakeBooleanValue('ye', true); + testMakeBooleanValue('yes', true); + }); + + it('should throw an error if text is not one of the above listed values', () => { + expect(() => TableUtils.makeBooleanValue('test')).toThrowError( + `Invalid boolean 'test'` + ); + }); +}); + +describe('makeValue', () => { + const testMakeValue = ( + columnType: string, + text: string, + expectedValue: unknown, + timeZone = 'America/New_York' + ) => { + expect(TableUtils.makeValue(columnType, text, timeZone)).toEqual( + expectedValue + ); + }; + it('should return null if text is "null"', () => { + testMakeValue('decimal', 'null', null); + }); + + it('should return text if columnType is string or char', () => { + testMakeValue('char', 'test', 'test'); + testMakeValue('java.lang.Character', 'test', 'test'); + testMakeValue('java.lang.String', 'test', 'test'); + }); + + it('should return a LongWrapper if columnType is long', () => { + const expectedLongWrapper = { + asNumber: expect.any(Function), + valueOf: expect.any(Function), + toString: expect.any(Function), + ofString: expect.any(Function), + }; + dh.LongWrapper = { + asNumber: jest.fn(), + valueOf: jest.fn(), + toString: jest.fn(), + ofString: jest.fn( + (str: string): LongWrapper => ({ + asNumber: jest.fn(), + valueOf: jest.fn(), + toString: jest.fn(), + ofString: jest.fn(), + }) + ), + }; + expect( + TableUtils.makeValue('long', 'test', 'America/New_York') + ).toMatchObject(expectedLongWrapper); + expect( + TableUtils.makeValue('java.lang.Long', 'test', 'America/New_York') + ).toMatchObject(expectedLongWrapper); + }); + + it('should return a boolean value if columnType is boolean', () => { + testMakeValue('boolean', '', null); + testMakeValue('java.lang.Boolean', 'null', null); + testMakeValue('boolean', 'true', true); + testMakeValue('boolean', 'false', false); + }); + + it('should return a DateWrapper object if columnType is date', () => { + const now = new Date(Date.now()); + const currentDate = DateUtils.makeDateWrapper( + 'America/New_York', + now.getFullYear(), + now.getMonth(), + now.getDate() + ); + const yesterdayDate = DateUtils.makeDateWrapper( + 'America/New_York', + now.getFullYear(), + now.getMonth(), + now.getDate() - 1 + ); + testMakeValue( + 'io.deephaven.db.tables.utils.DBDateTime', + 'today', + currentDate + ); + testMakeValue('io.deephaven.time.DateTime', 'null', null); + testMakeValue( + 'com.illumon.iris.db.tables.utils.DBDateTime', + 'yesterday', + yesterdayDate + ); + }); + + it('should return a number if columnType is number', () => { + testMakeValue('int', '2', 2); + testMakeValue('java.lang.Integer', '2', 2); + testMakeValue('java.math.BigInteger', '2222222222222222', 2222222222222222); + testMakeValue('short', '32767', 32767); + testMakeValue('java.lang.Short', '32767', 32767); + testMakeValue('byte', '127', 127); + testMakeValue('java.lang.Byte', '127', 127); + + testMakeValue('double', '1.1111111111', 1.1111111111); + testMakeValue('java.lang.Double', '1.1111111111', 1.1111111111); + testMakeValue( + 'java.math.BigDecimal', + '123.11111111111111', + 123.11111111111111 + ); + testMakeValue('float', '1.111111111', 1.111111111); + testMakeValue('java.lang.Float', '1.111111111', 1.111111111); + }); + + it('returns null if the column type does not match any of the types', () => { + testMakeValue('invalid_type', 'test', null); + }); +}); + +describe('getFilterText', () => { + it('should return the filter text', () => { + const filter = makeFilterCondition(); + filter.toString = jest.fn(() => 'text'); + expect(TableUtils.getFilterText(filter as FilterCondition)).toBe('text'); + }); + + it('should return null if filter is null', () => { + expect(TableUtils.getFilterText()).toBeNull(); + expect(TableUtils.getFilterText(null)).toBeNull(); + }); +}); + +describe('getFilterTypes', () => { + const testGetFilterTypes = ( + columnTypes: string[], + expectedArray: FilterTypeValue[] + ) => { + columnTypes.forEach(element => { + expect(TableUtils.getFilterTypes(element)).toEqual(expectedArray); + }); + }; + + it('should return the valid filter types for boolean column type', () => { + const columnTypes = ['boolean', 'java.lang.Boolean']; + testGetFilterTypes(columnTypes, ['isTrue', 'isFalse', 'isNull']); + }); + + it('should return the valid filter types for char, number, or date column type', () => { + const columnTypes = [ + 'char', + 'java.lang.Character', + 'int', + 'java.lang.Integer', + 'java.math.BigInteger', + 'long', + 'java.lang.Long', + 'short', + 'java.lang.Short', + 'byte', + 'java.lang.Byte', + 'double', + 'java.lang.Double', + 'java.math.BigDecimal', + 'float', + 'java.lang.Float', + 'io.deephaven.db.tables.utils.DBDateTime', + 'io.deephaven.time.DateTime', + 'com.illumon.iris.db.tables.utils.DBDateTime', + ]; + const expectedArray: FilterTypeValue[] = [ + 'eq', + 'notEq', + 'greaterThan', + 'greaterThanOrEqualTo', + 'lessThan', + 'lessThanOrEqualTo', + ]; + testGetFilterTypes(columnTypes, expectedArray); + }); + + it('should return the valid filter types for text column type', () => { + const columnTypes = ['java.lang.String']; + const expectedArray: FilterTypeValue[] = [ + 'eq', + 'eqIgnoreCase', + 'notEq', + 'notEqIgnoreCase', + 'contains', + 'notContains', + 'startsWith', + 'endsWith', + ]; + testGetFilterTypes(columnTypes, expectedArray); + }); + + it('should return an empty array if the column type is not one of the types listed above', () => { + testGetFilterTypes(['test'], []); + }); +}); + +describe('makeColumnSort', () => { + const testMakeColumnSort = ( + columns: readonly Column[], + columnIndex: number, + direction: SortDirection, + isAbs: boolean, + expectedValue: Partial | null + ) => { + expect( + TableUtils.makeColumnSort(columns, columnIndex, direction, isAbs) + ).toEqual(expectedValue); + }; + + it('should return null if columnIndex is less than 0, or columnIndex is greater than or equal to columns.length', () => { + const columns = makeColumns(); + testMakeColumnSort(columns, -1, 'ASC', true, null); + testMakeColumnSort(columns, 6, 'ASC', true, null); + }); + + it('should return null if direction is null', () => { + const columns = makeColumns(); + testMakeColumnSort(columns, 0, null, true, null); + }); + + it('should return an ascending sort if direction is ASC', () => { + const columns = makeColumns(); + const expectedValue: Partial = { + column: columns[0], + direction: 'ASC', + isAbs: true, + }; + testMakeColumnSort(columns, 0, 'ASC', true, expectedValue); + }); + + it('should return an descending sort if direction is DESC', () => { + const columns = makeColumns(); + const expectedValue: Partial = { + column: columns[1], + direction: 'DESC', + isAbs: false, + }; + testMakeColumnSort(columns, 1, 'DESC', false, expectedValue); + }); + + it('should return the default sort if direction is REVERSE', () => { + const columns = makeColumns(); + const expectedValue: Partial = { + column: columns[2], + direction: 'ASC', + isAbs: true, + }; + testMakeColumnSort(columns, 2, 'REVERSE', true, expectedValue); + }); +}); + +describe('sortColumns', () => { + it('should sort the columns in ascending order based on column name', () => { + const columns = makeColumns(); + expect(TableUtils.sortColumns(columns, true)).toEqual(columns); + }); + + it('should sort the columns in descending order based on column name', () => { + const columns = makeColumns(); + const reversed = columns.reverse(); + + expect(TableUtils.sortColumns(columns, false)).toEqual(reversed); + }); +}); + +describe('getNextSort', () => { + const testGetNextSort = ( + columns: readonly Column[], + sorts: readonly Sort[], + columnIndex: number, + expectedValue + ) => { + expect(TableUtils.getNextSort(columns, sorts, columnIndex)).toEqual( + expectedValue + ); + }; + it('should return null if columnIndex is out of range', () => { + const columns = makeColumns(); + testGetNextSort(columns, [], -1, null); + testGetNextSort(columns, [], 10, null); + }); +}); + +describe('sortColumn', () => { + const testSortColumn = ( + sorts: readonly Sort[], + columns: readonly Column[], + modelColumn: number, + direction: SortDirection, + isAbs: boolean, + addToExisting: boolean, + expectedValue + ) => { + expect( + TableUtils.sortColumn( + sorts, + columns, + modelColumn, + direction, + isAbs, + addToExisting + ) + ).toEqual(expectedValue); + }; + + it('should return an empty array if columnIndex is out of range', () => { + const columns = makeColumns(); + testSortColumn([], columns, -1, 'ASC', true, true, []); + testSortColumn([], columns, 10, 'ASC', true, true, []); + }); + + it('applies sort properly', () => { + const columns = makeColumns(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const table: Table = new (dh as any).Table({ columns }); + let tableSorts: Sort[] = []; + + expect(table).not.toBe(null); + expect(table.sort.length).toBe(0); + + tableSorts = TableUtils.sortColumn( + tableSorts, + columns, + 0, + 'ASC', + true, + true + ); + table.applySort(tableSorts); + expect(table.sort.length).toBe(1); + expect(table.sort[0].column).toBe(columns[0]); + expect(table.sort[0].direction).toBe(TableUtils.sortDirection.ascending); + expect(table.sort[0].isAbs).toBe(true); + + tableSorts = TableUtils.sortColumn( + tableSorts, + columns, + 3, + 'ASC', + false, + true + ); + table.applySort(tableSorts); + expect(table.sort.length).toBe(2); + expect(table.sort[0].column).toBe(columns[0]); + expect(table.sort[0].direction).toBe(TableUtils.sortDirection.ascending); + expect(table.sort[0].isAbs).toBe(true); + expect(table.sort[1].column).toBe(columns[3]); + expect(table.sort[1].direction).toBe(TableUtils.sortDirection.ascending); + expect(table.sort[1].isAbs).toBe(false); + + tableSorts = TableUtils.sortColumn( + tableSorts, + columns, + 0, + 'DESC', + false, + true + ); + table.applySort(tableSorts); + expect(table.sort.length).toBe(2); + expect(table.sort[0].column).toBe(columns[3]); + expect(table.sort[0].direction).toBe(TableUtils.sortDirection.ascending); + expect(table.sort[0].isAbs).toBe(false); + expect(table.sort[1].column).toBe(columns[0]); + expect(table.sort[1].direction).toBe(TableUtils.sortDirection.descending); + expect(table.sort[1].isAbs).toBe(false); + + tableSorts = TableUtils.sortColumn( + tableSorts, + columns, + 3, + 'DESC', + false, + false + ); + table.applySort(tableSorts); + expect(table.sort.length).toBe(1); + expect(table.sort[0].column).toBe(columns[3]); + expect(table.sort[0].direction).toBe(TableUtils.sortDirection.descending); + expect(table.sort[0].isAbs).toBe(false); + }); +}); + +describe('getNormalizedType', () => { + const testGetNormalizedType = ( + columnType: string | null, + expectedValue: DataType + ) => { + expect(TableUtils.getNormalizedType(columnType)).toBe(expectedValue); + }; + + it('returns the boolean data type for boolean column type', () => { + testGetNormalizedType('boolean', 'boolean'); + testGetNormalizedType('java.lang.Boolean', 'boolean'); + }); + + it('returns the character data type for character column type', () => { + testGetNormalizedType('char', 'char'); + testGetNormalizedType('java.lang.Character', 'char'); + }); + + it('returns the string data type for string column type', () => { + testGetNormalizedType('string', 'string'); + testGetNormalizedType('java.lang.String', 'string'); + }); + + it('returns the date time data type for date time column type', () => { + testGetNormalizedType( + 'io.deephaven.db.tables.utils.DBDateTime', + 'datetime' + ); + testGetNormalizedType('io.deephaven.time.DateTime', 'datetime'); + testGetNormalizedType( + 'com.illumon.iris.db.tables.utils.DBDateTime', + 'datetime' + ); + testGetNormalizedType('datetime', 'datetime'); + }); + + it('returns the decimal data type for double, float, and bigdecimal column type', () => { + testGetNormalizedType('double', 'decimal'); + testGetNormalizedType('java.lang.Double', 'decimal'); + testGetNormalizedType('float', 'decimal'); + testGetNormalizedType('java.lang.Float', 'decimal'); + testGetNormalizedType('java.math.BigDecimal', 'decimal'); + testGetNormalizedType('decimal', 'decimal'); + }); + + it('returns the int data type for int, long, short, byte, and biginteger column type', () => { + testGetNormalizedType('int', 'int'); + testGetNormalizedType('java.lang.Integer', 'int'); + testGetNormalizedType('long', 'int'); + testGetNormalizedType('java.lang.Long', 'int'); + testGetNormalizedType('short', 'int'); + testGetNormalizedType('java.lang.Short', 'int'); + testGetNormalizedType('byte', 'int'); + testGetNormalizedType('java.lang.Byte', 'int'); + testGetNormalizedType('java.math.BigInteger', 'int'); + }); + + it('returns unknown for any unknown column type', () => { + testGetNormalizedType('test', 'unknown'); + testGetNormalizedType('unknown', 'unknown'); + testGetNormalizedType(null, 'unknown'); + }); +}); + +describe('isBigDecimalType', () => { + it('should return true if the column type is big decimal', () => { + expect(TableUtils.isBigDecimalType('java.math.BigDecimal')).toBe(true); + }); + + it('should return false if column type is not big decimal', () => { + expect(TableUtils.isBigDecimalType('test')).toBe(false); + }); +}); + +describe('isBigIntegerType', () => { + it('should return true if the column type is big integer', () => { + expect(TableUtils.isBigIntegerType('java.math.BigInteger')).toBe(true); + }); + + it('should return false if column type is not big integer', () => { + expect(TableUtils.isBigIntegerType('test')).toBe(false); + }); +}); + +describe('getBaseType', () => { + it('should return the base column type', () => { + expect(TableUtils.getBaseType('test')).toBe('test'); + expect(TableUtils.getBaseType('test1[]test2')).toBe('test1'); + }); +}); + +describe('isCompatibleType', () => { + it('should return true if two types are compatible', () => { + expect(TableUtils.isCompatibleType('boolean', 'java.lang.Boolean')).toBe( + true + ); + expect(TableUtils.isCompatibleType()).toBe(true); + expect(TableUtils.isCompatibleType('int', 'long')).toBe(true); + }); + + it('should return false if two types are not compatible', () => { + expect(TableUtils.isCompatibleType('boolean', 'int')).toBe(false); + expect(TableUtils.isCompatibleType('boolean')).toBe(false); + expect(TableUtils.isCompatibleType('int', 'double')).toBe(false); + }); +}); diff --git a/packages/jsapi-utils/src/TableUtils.ts b/packages/jsapi-utils/src/TableUtils.ts index f4078b9b96..1d9700e31a 100644 --- a/packages/jsapi-utils/src/TableUtils.ts +++ b/packages/jsapi-utils/src/TableUtils.ts @@ -162,7 +162,7 @@ export class TableUtils { sorts: readonly Sort[], columnIndex: number ): Sort | null { - if (columns == null || columnIndex < 0 || columnIndex >= columns.length) { + if (columnIndex < 0 || columnIndex >= columns.length) { return null; } @@ -182,7 +182,7 @@ export class TableUtils { direction: SortDirection, isAbs: boolean ): Sort | null { - if (columns == null || columnIndex < 0 || columnIndex >= columns.length) { + if (columnIndex < 0 || columnIndex >= columns.length) { return null; } @@ -243,7 +243,7 @@ export class TableUtils { isAbs: boolean, addToExisting: boolean ): Sort[] { - if (sorts == null || modelColumn < 0 || modelColumn >= columns.length) { + if (modelColumn < 0 || modelColumn >= columns.length) { return []; } @@ -555,10 +555,6 @@ export class TableUtils { column: Column, text: string ): FilterCondition | null { - if (text == null) { - return null; - } - const columnFilter = column.filter(); let filter = null; @@ -649,12 +645,8 @@ export class TableUtils { static makeQuickTextFilter( column: Column, - text: string | null + text: string ): FilterCondition | null { - if (text == null) { - return null; - } - const cleanText = `${text}`.trim(); const regex = /^(!~|!=|~|=|!)?(.*)/; const result = regex.exec(cleanText); @@ -796,10 +788,6 @@ export class TableUtils { column: Column, text: string | number ): FilterCondition | null { - if (text == null) { - return null; - } - const regex = /^(!=|=|!)?(.*)/; const result = regex.exec(`${text}`.trim()); if (result === null) { @@ -898,10 +886,6 @@ export class TableUtils { operation: FilterTypeValue, timeZone: string ): FilterCondition { - if (column == null) { - throw new Error('Column is null'); - } - const [startDate, endDate] = DateUtils.parseDateRange(text, timeZone); const startValue = @@ -988,10 +972,6 @@ export class TableUtils { column: Column, text: string ): FilterCondition | null { - if (text == null) { - return null; - } - const cleanText = `${text}`.trim(); const regex = /^(>=|<=|=>|=<|>|<|!=|=|!)?(null|"."|'.'|.)?(.*)/; const result = regex.exec(cleanText); @@ -1265,7 +1245,7 @@ export class TableUtils { text: string, timeZone: string ): string | number | boolean | LongWrapper | null { - if (text == null || text === 'null') { + if (text === 'null') { return null; } if (TableUtils.isTextType(columnType)) { @@ -1322,7 +1302,7 @@ export class TableUtils { } static makeNumberValue(text: string): number | null { - if (text == null || text === 'null' || text === '') { + if (text === 'null' || text === '') { return null; } diff --git a/packages/jsapi-utils/src/formatters/BooleanColumnFormatter.test.ts b/packages/jsapi-utils/src/formatters/BooleanColumnFormatter.test.ts new file mode 100644 index 0000000000..64d5c68ae9 --- /dev/null +++ b/packages/jsapi-utils/src/formatters/BooleanColumnFormatter.test.ts @@ -0,0 +1,20 @@ +import BooleanColumnFormatter from './BooleanColumnFormatter'; + +describe('format', () => { + it('should return "true" when the value is 1 or true', () => { + const formatter = new BooleanColumnFormatter(); + expect(formatter.format(1)).toBe('true'); + expect(formatter.format(true)).toBe('true'); + }); + + it('should return "false" when the value is 0 or false', () => { + const formatter = new BooleanColumnFormatter(); + expect(formatter.format(0)).toBe('false'); + expect(formatter.format(false)).toBe('false'); + }); + + it('should return the empty string when the value is not 0, 1, true, or false', () => { + const formatter = new BooleanColumnFormatter(); + expect(formatter.format(2)).toBe(''); + }); +}); diff --git a/packages/jsapi-utils/src/formatters/BooleanColumnFormatter.ts b/packages/jsapi-utils/src/formatters/BooleanColumnFormatter.ts index 82d7623b37..6861d8f8a7 100644 --- a/packages/jsapi-utils/src/formatters/BooleanColumnFormatter.ts +++ b/packages/jsapi-utils/src/formatters/BooleanColumnFormatter.ts @@ -2,7 +2,7 @@ /* eslint no-unused-vars: "off" */ import TableColumnFormatter from './TableColumnFormatter'; -/** Column formatter for chars */ +/** Column formatter for booleans */ class BooleanColumnFormatter extends TableColumnFormatter { format(value: boolean | number): string { switch (value) { diff --git a/packages/jsapi-utils/src/formatters/CharColumnFormatter.test.ts b/packages/jsapi-utils/src/formatters/CharColumnFormatter.test.ts new file mode 100644 index 0000000000..103c0680a0 --- /dev/null +++ b/packages/jsapi-utils/src/formatters/CharColumnFormatter.test.ts @@ -0,0 +1,10 @@ +import CharColumnFormatter from './CharColumnFormatter'; + +describe('format', () => { + it('should return a string respresentation of the character code', () => { + const formatter = new CharColumnFormatter(); + expect(formatter.format(48)).toBe('0'); + expect(formatter.format(65)).toBe('A'); + expect(formatter.format(97)).toBe('a'); + }); +}); diff --git a/packages/jsapi-utils/src/formatters/DateTimeColumnFormatter.test.ts b/packages/jsapi-utils/src/formatters/DateTimeColumnFormatter.test.ts index 5f649feea6..4cef77396d 100644 --- a/packages/jsapi-utils/src/formatters/DateTimeColumnFormatter.test.ts +++ b/packages/jsapi-utils/src/formatters/DateTimeColumnFormatter.test.ts @@ -1,5 +1,6 @@ import dh, { TimeZone } from '@deephaven/jsapi-shim'; import DateTimeColumnFormatter from './DateTimeColumnFormatter'; +import { TableColumnFormat } from './TableColumnFormatter'; function makeFormatter({ timeZone, @@ -36,6 +37,17 @@ const { DEFAULT_TIME_ZONE_ID, } = DateTimeColumnFormatter; +const VALID_FORMATS = [ + DEFAULT_DATETIME_FORMAT_STRING, + 'yyyy-MM-dd HH:mm:ss', + 'yyyy-MM-dd HH:mm:ss.SSSSSSSSS', + 'yyyy-MM-dd', + 'MM-dd-yyyy', + 'HH:mm:ss', + 'HH:mm:ss.SSS', + 'HH:mm:ss.SSSSSSSSS', +]; + it('should not throw if constructor called with no arguments', () => { expect(makeFormatter).not.toThrow(); }); @@ -181,3 +193,139 @@ describe('calls to iris format and time zone functions', () => { expect(formatMock.mock.calls[1][2]).toMatchObject(defaultTimeZone); }); }); + +describe('isValid', () => { + it('should return true if a format is valid', () => { + for (let i = 0; i < VALID_FORMATS.length; i += 1) { + expect( + DateTimeColumnFormatter.isValid({ + formatString: VALID_FORMATS[i], + }) + ).toBe(true); + } + }); +}); + +describe('isSameFormat', () => { + it('should return true if two formats are the same excluding label', () => { + const format1: TableColumnFormat = { + label: 'format1', + formatString: 'yyyy-MM-dd HH:mm:ss', + type: 'type-context-custom', + }; + const format2: TableColumnFormat = { + label: 'format2', + formatString: 'yyyy-MM-dd HH:mm:ss', + type: 'type-context-custom', + }; + + expect(DateTimeColumnFormatter.isSameFormat(format1, format2)).toBe(true); + }); + + it('should return false if two formats are different excluding label', () => { + const format1: TableColumnFormat = { + label: 'format1', + formatString: 'yyyy-MM-dd HH:mm:ss', + type: 'type-context-preset', + }; + const format2: TableColumnFormat = { + label: 'format2', + formatString: 'yyyy-MM-dd HH:mm:ss', + type: 'type-context-custom', + }; + + expect(DateTimeColumnFormatter.isSameFormat(format1, format2)).toBe(false); + }); +}); + +describe('makeGlobalFormatStringMap', () => { + const mapsAreEqual = (m1, m2) => + m1.size === m2.size && + Array.from(m1.keys()).every(key => m1.get(key) === m2.get(key)); + + it('should return a global format string map without showing Timezone or Tseparator', () => { + const expectedMap = new Map([ + ['yyyy-MM-dd HH:mm:ss', `yyyy-MM-dd HH:mm:ss`], + ['yyyy-MM-dd HH:mm:ss.SSS', `yyyy-MM-dd HH:mm:ss.SSS`], + ['yyyy-MM-dd HH:mm:ss.SSSSSSSSS', `yyyy-MM-dd HH:mm:ss.SSSSSSSSS`], + ]); + + expect( + mapsAreEqual( + DateTimeColumnFormatter.makeGlobalFormatStringMap(false, false), + expectedMap + ) + ).toBe(true); + }); + + it('should return a global format string map showing Timezone but not Tseparator', () => { + const expectedMap = new Map([ + ['yyyy-MM-dd HH:mm:ss', `yyyy-MM-dd HH:mm:ss z`], + ['yyyy-MM-dd HH:mm:ss.SSS', `yyyy-MM-dd HH:mm:ss.SSS z`], + ['yyyy-MM-dd HH:mm:ss.SSSSSSSSS', `yyyy-MM-dd HH:mm:ss.SSSSSSSSS z`], + ]); + + expect( + mapsAreEqual( + DateTimeColumnFormatter.makeGlobalFormatStringMap(true, false), + expectedMap + ) + ).toBe(true); + }); + + it('should return a global format string map showing Tseparator but not Timezone', () => { + const expectedMap = new Map([ + ['yyyy-MM-dd HH:mm:ss', `yyyy-MM-dd'T'HH:mm:ss`], + ['yyyy-MM-dd HH:mm:ss.SSS', `yyyy-MM-dd'T'HH:mm:ss.SSS`], + ['yyyy-MM-dd HH:mm:ss.SSSSSSSSS', `yyyy-MM-dd'T'HH:mm:ss.SSSSSSSSS`], + ]); + + expect( + mapsAreEqual( + DateTimeColumnFormatter.makeGlobalFormatStringMap(false, true), + expectedMap + ) + ).toBe(true); + }); + + it('should return a global format string map show Tseparator and Timezone', () => { + const expectedMap = new Map([ + ['yyyy-MM-dd HH:mm:ss', `yyyy-MM-dd'T'HH:mm:ss z`], + ['yyyy-MM-dd HH:mm:ss.SSS', `yyyy-MM-dd'T'HH:mm:ss.SSS z`], + ['yyyy-MM-dd HH:mm:ss.SSSSSSSSS', `yyyy-MM-dd'T'HH:mm:ss.SSSSSSSSS z`], + ]); + + expect( + mapsAreEqual( + DateTimeColumnFormatter.makeGlobalFormatStringMap(true, true), + expectedMap + ) + ).toBe(true); + }); +}); + +describe('getGlobalFormats', () => { + it('should get global formats', () => { + const expectedArray = [ + 'yyyy-MM-dd HH:mm:ss', + 'yyyy-MM-dd HH:mm:ss.SSS', + 'yyyy-MM-dd HH:mm:ss.SSSSSSSSS', + ]; + + expect(DateTimeColumnFormatter.getGlobalFormats(false, false)).toEqual( + expectedArray + ); + expect(DateTimeColumnFormatter.getGlobalFormats(true, false)).toEqual( + expectedArray + ); + expect(DateTimeColumnFormatter.getGlobalFormats(false, true)).toEqual( + expectedArray + ); + expect(DateTimeColumnFormatter.getGlobalFormats(true, true)).toEqual( + expectedArray + ); + }); +}); + +// getGlobalFormats parameters doesn't change anything +// Not sure how to test the catch statements diff --git a/packages/jsapi-utils/src/formatters/DefaultColumnFormatter.test.ts b/packages/jsapi-utils/src/formatters/DefaultColumnFormatter.test.ts new file mode 100644 index 0000000000..61078c345c --- /dev/null +++ b/packages/jsapi-utils/src/formatters/DefaultColumnFormatter.test.ts @@ -0,0 +1,10 @@ +import DefaultColumnFormatter from './DefaultColumnFormatter'; + +describe('format', () => { + it('should return a string containing the given value', () => { + const formatter = new DefaultColumnFormatter(); + expect(formatter.format(null)).toBe('null'); + expect(formatter.format(2)).toBe('2'); + expect(formatter.format('test')).toBe('test'); + }); +}); diff --git a/packages/jsapi-utils/src/formatters/NumberColumnFormatter.test.ts b/packages/jsapi-utils/src/formatters/NumberColumnFormatter.test.ts index 550af4d70b..7378817bdd 100644 --- a/packages/jsapi-utils/src/formatters/NumberColumnFormatter.test.ts +++ b/packages/jsapi-utils/src/formatters/NumberColumnFormatter.test.ts @@ -51,4 +51,68 @@ numberColumnFormatters.forEach(({ name, formatter: NumberColumnFormatter }) => { expect(formatter.format(null)).toBe(''); }); }); + + describe(`${name}.makePresetFormat`, () => { + it('should return an object with preset type', () => { + const expectedObject = { + label: 'test', + formatString: '##0.00%', + type: 'type-context-preset', + multiplier: 2, + }; + + expect( + NumberColumnFormatter.makePresetFormat('test', '##0.00%', 2) + ).toEqual(expectedObject); + }); + }); + + describe(`${name}.makeCustomFormat`, () => { + it('should return an object with preset type', () => { + const expectedObject = { + label: 'Custom Format', + formatString: '##0.00%', + type: 'type-context-custom', + multiplier: 2, + }; + + expect(NumberColumnFormatter.makeCustomFormat('##0.00%', 2)).toEqual( + expectedObject + ); + }); + }); + + describe(`${name}.isSameFormat`, () => { + it('should return true if two format objects are the same excluding label', () => { + const format1 = NumberColumnFormatter.makeFormat( + 'format1', + '##0.00%', + 'type-context-custom', + 2 + ); + const format2 = NumberColumnFormatter.makeFormat( + 'format2', + '##0.00%', + 'type-context-custom', + 2 + ); + expect(NumberColumnFormatter.isSameFormat(format1, format2)).toBe(true); + }); + + it('should return false if two format objects are different excluding label', () => { + const format1 = NumberColumnFormatter.makeFormat( + 'format1', + '##0.00%', + 'type-context-custom', + 2 + ); + const format2 = NumberColumnFormatter.makeFormat( + 'format2', + '##0.000%', + 'type-context-preset', + 3 + ); + expect(NumberColumnFormatter.isSameFormat(format1, format2)).toBe(false); + }); + }); }); diff --git a/packages/jsapi-utils/src/formatters/StringColumnFormatter.test.ts b/packages/jsapi-utils/src/formatters/StringColumnFormatter.test.ts new file mode 100644 index 0000000000..6475b54da6 --- /dev/null +++ b/packages/jsapi-utils/src/formatters/StringColumnFormatter.test.ts @@ -0,0 +1,8 @@ +import StringColumnFormatter from './StringColumnFormatter'; + +describe('format', () => { + it('should return a string containing the given value', () => { + const formatter = new StringColumnFormatter(); + expect(formatter.format('test')).toBe('test'); + }); +}); diff --git a/packages/jsapi-utils/src/formatters/TableColumnFormatter.test.ts b/packages/jsapi-utils/src/formatters/TableColumnFormatter.test.ts new file mode 100644 index 0000000000..b0934226e5 --- /dev/null +++ b/packages/jsapi-utils/src/formatters/TableColumnFormatter.test.ts @@ -0,0 +1,38 @@ +import TableColumnFormatter, { + TableColumnFormat, +} from './TableColumnFormatter'; + +const VALID_FORMAT: TableColumnFormat = { + label: 'test', + formatString: '0.0', + type: 'type-context-custom', +}; + +describe('isValid', () => { + it('should return true', () => { + expect(TableColumnFormatter.isValid(VALID_FORMAT)).toBe(true); + }); +}); + +describe('isSameFormat', () => { + it('should throw an error', () => { + expect(() => + TableColumnFormatter.isSameFormat(VALID_FORMAT, VALID_FORMAT) + ).toThrowError('isSameFormat not implemented'); + }); +}); + +describe('makeFormat', () => { + it('returns a TableColumnFormat object with the given arguments', () => { + expect( + TableColumnFormatter.makeFormat('test', '0.0', 'type-context-custom') + ).toEqual(VALID_FORMAT); + }); +}); + +describe('format', () => { + it('returns an empty string', () => { + const formatter = new TableColumnFormatter(); + expect(formatter.format('test')).toBe(''); + }); +});