Skip to content
Merged
168 changes: 167 additions & 1 deletion projects/igniteui-angular/core/src/core/utils.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { SampleTestData } from 'igniteui-angular/test-utils/sample-test-data.spec';
import { cloneValue, isObject, isDate } from './utils';
import { cloneValue, cloneValueCached, cloneHierarchicalArray, compareMaps, getComponentCssSizeVar,
intoChunks, isDate, isLeftToRight, isObject, showMessage, uniqueDates } from './utils';

describe('Utils', () => {
const complexObject = {
Expand Down Expand Up @@ -169,6 +170,171 @@ describe('Utils', () => {
const undefinedClone = cloneValue(undefined);
expect(undefinedClone).toBeUndefined();
});

it('Should skip the `externalObject` key', () => {
const source = { Number: 1, externalObject: { framework: 'reference that must not be cloned' } };

const clone = cloneValue(source);

expect(clone.Number).toBe(1);
expect('externalObject' in clone).toBeFalsy();
});
});

describe('Utils - cloneValueCached() unit tests', () => {
it('Should clone primitives, dates and arrays the same way cloneValue does', () => {
const cache = new Map<any, any>();
const date = new Date(10 * 1000 * 60 * 60 * 24);
const array = [1, { a: 1 }, 'string'];

expect(cloneValueCached(1, cache)).toBe(1);
expect(cloneValueCached('string', cache)).toBe('string');
expect(cloneValueCached(true, cache)).toBe(true);
expect(cloneValueCached(null, cache)).toBeNull();
expect(cloneValueCached(undefined, cache)).toBeUndefined();

const clonedDate = cloneValueCached(date, cache);
expect(clonedDate).toEqual(date);
expect(clonedDate).not.toBe(date);

// Arrays are shallow copied - the array itself is new, its items are not.
const clonedArray = cloneValueCached(array, cache);
expect(clonedArray).toEqual(array);
expect(clonedArray).not.toBe(array);
expect(clonedArray[1]).toBe(array[1]);
});

it('Should not clone Map or Set', () => {
const cache = new Map<any, any>();
const map = new Map([['key', 'value']]);
const set = new Set(['value']);

expect(cloneValueCached(map, cache)).toBe(map);
expect(cloneValueCached(set, cache)).toBe(set);
});

it('Should deep clone objects', () => {
const cache = new Map<any, any>();
const clone = cloneValueCached(complexObject, cache);

expect(clone).toEqual(complexObject);
expect(clone).not.toBe(complexObject);
expect(clone.Object10).not.toBe(complexObject.Object10);
expect(clone.Object10.Object100).not.toBe(complexObject.Object10.Object100);
});

it('Should reuse the cached clone for repeated references', () => {
const cache = new Map<any, any>();
const shared = { value: 'shared' };
const source = { first: shared, second: shared };

const clone = cloneValueCached(source, cache);

expect(clone).toEqual(source);
// The same source reference has to resolve to the same clone, not to two separate copies.
expect(clone.first).toBe(clone.second);
expect(clone.first).not.toBe(shared);
});

it('Should handle circular references', () => {
const cache = new Map<any, any>();
const source: any = { name: 'root' };
source.self = source;
source.child = { parent: source };

const clone = cloneValueCached(source, cache);

expect(clone.name).toBe('root');
expect(clone.self).toBe(clone);
expect(clone.child.parent).toBe(clone);
expect(clone).not.toBe(source);
});
});

describe('Utils - uniqueDates() unit tests', () => {
it('Should keep only the first entry for every distinct label', () => {
const first = { label: '1/1/2024', value: new Date(2024, 0, 1) };
const duplicate = { label: '1/1/2024', value: new Date(2024, 0, 1) };
const second = { label: '2/1/2024', value: new Date(2024, 1, 1) };

expect(uniqueDates([first, duplicate, second, second])).toEqual([first, second]);
expect(uniqueDates([])).toEqual([]);
});
});

describe('Utils - compareMaps() unit tests', () => {
it('Should compare maps by size, keys and values', () => {
const map = new Map([['a', 1], ['b', 2]]);

expect(compareMaps(map, new Map([['a', 1], ['b', 2]]))).toBeTruthy('equal maps');
expect(compareMaps(map, new Map([['a', 1], ['b', 3]]))).toBeFalsy('different value');
expect(compareMaps(map, new Map([['a', 1], ['c', 2]]))).toBeFalsy('different key');
expect(compareMaps(map, new Map([['a', 1]]))).toBeFalsy('different size');
expect(compareMaps(new Map(), new Map())).toBeTruthy('two empty maps');
});

it('Should treat a missing second map as equal only to a missing first one', () => {
expect(compareMaps(null, null)).toBeTruthy('both missing');
expect(compareMaps(new Map([['a', 1]]), null)).toBeFalsy('only the second one missing');
});
});

describe('Utils - intoChunks() unit tests', () => {
it('Should split an array into chunks of the requested size', () => {
const array = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];

expect(Array.from(intoChunks(array, 2))).toEqual([[0, 1], [2, 3], [4, 5], [6, 7], [8, 9]]);
// The last chunk holds the remainder when the size is not a divisor of the length.
expect(Array.from(intoChunks(array, 3))).toEqual([[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]]);
expect(Array.from(intoChunks(array, 20))).toEqual([array]);
expect(Array.from(intoChunks([], 3))).toEqual([]);
});

it('Should throw for a chunk size below one', () => {
expect(() => Array.from(intoChunks([1, 2, 3], 0))).toThrowError('size must be an integer >= 1');
expect(() => Array.from(intoChunks([1, 2, 3], -3))).toThrowError('size must be an integer >= 1');
});
});

describe('Utils - getComponentCssSizeVar() unit tests', () => {
it('Should map the numeric size to the matching CSS variable', () => {
expect(getComponentCssSizeVar('1')).toBe('var(--ig-size, var(--ig-size-small))');
expect(getComponentCssSizeVar('2')).toBe('var(--ig-size, var(--ig-size-medium))');
expect(getComponentCssSizeVar('3')).toBe('var(--ig-size, var(--ig-size-large))');
// Anything unrecognized falls back to the large size.
expect(getComponentCssSizeVar('')).toBe('var(--ig-size, var(--ig-size-large))');
});
});

describe('Utils - showMessage() unit tests', () => {
it('Should always report the message as shown', () => {
// The warning itself is a console side effect - what the callers act on is the returned flag,
// which has to stay `true` both for the call that logs and for the one that skips it.
expect(showMessage('Deprecated', false)).toBeTruthy('not shown yet');
expect(showMessage('Deprecated', true)).toBeTruthy('already shown');
});
});

describe('Utils - cloneHierarchicalArray() unit tests', () => {
it('Should clone nested arrays and tolerate a missing source', () => {
const source = [
{ id: 1, children: [{ id: 11, children: [] }] },
{ id: 2 }
];

const clone = cloneHierarchicalArray(source, 'children');
expect(clone).toEqual(source);
expect(clone).not.toBe(source);
expect(clone[0].children).not.toBe(source[0].children);

expect(cloneHierarchicalArray(null, 'children')).toEqual([]);
});
});

describe('Utils - isLeftToRight() unit tests', () => {
it('Should default to left-to-right when there is no element', () => {
expect(isLeftToRight(null)).toBeTruthy('no element');
});
});

describe('Utils - isObject() unit tests', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { IgxStringFilteringOperand,
IgxNumberFilteringOperand,
IgxDateFilteringOperand,
IgxDateTimeFilteringOperand,
IgxTimeFilteringOperand,
IgxBooleanFilteringOperand,
IgxFilteringOperand} from './filtering-condition';

Expand Down Expand Up @@ -143,6 +145,194 @@ describe('Unit testing FilteringCondition', () => {
expect(!f.condition('notNull').logic(null) && f.condition('notNull').logic(undefined) && f.condition('notNull').logic(false))
.toBeTruthy('notNull');
});
it('tests dateTime conditions', () => {
const fdt = IgxDateTimeFilteringOperand.instance();
jasmine.clock().install();
try {
jasmine.clock().mockDate(new Date(2024, 5, 15));
const now = new Date();
const yesterday = ((d) => new Date(d.setDate(d.getDate() - 1)))(new Date());
const lastMonth = ((d) => {
d.setDate(1); return new Date(d.setMonth(d.getMonth() - 1));
})(new Date());
const nextMonth = ((d) => {
d.setDate(1); return new Date(d.setMonth(d.getMonth() + 1));
})(new Date());
const lastYear = ((d) => new Date(d.setFullYear(d.getFullYear() - 1)))(new Date());
const nextYear = ((d) => new Date(d.setFullYear(d.getFullYear() + 1)))(new Date());

expect(fdt.condition('before').logic(yesterday, now) &&
!fdt.condition('before').logic(now, yesterday) &&
!fdt.condition('before').logic(null, now))
.toBeTruthy('before');
expect(fdt.condition('after').logic(now, yesterday) &&
!fdt.condition('after').logic(yesterday, now) &&
!fdt.condition('after').logic(null, now))
.toBeTruthy('after');
expect(fdt.condition('today').logic(now) &&
!fdt.condition('today').logic(nextYear) &&
!fdt.condition('today').logic(null))
.toBeTruthy('today');
expect(fdt.condition('yesterday').logic(yesterday) &&
!fdt.condition('yesterday').logic(nextYear) &&
!fdt.condition('yesterday').logic(null))
.toBeTruthy('yesterday');
expect(fdt.condition('thisMonth').logic(now) &&
!fdt.condition('thisMonth').logic(nextYear) &&
!fdt.condition('thisMonth').logic(null))
.toBeTruthy('thisMonth');
expect(fdt.condition('lastMonth').logic(lastMonth) &&
!fdt.condition('lastMonth').logic(now) &&
!fdt.condition('lastMonth').logic(null))
.toBeTruthy('lastMonth');
expect(fdt.condition('nextMonth').logic(nextMonth) &&
!fdt.condition('nextMonth').logic(now) &&
!fdt.condition('nextMonth').logic(null))
.toBeTruthy('nextMonth');
expect(fdt.condition('thisYear').logic(now) &&
!fdt.condition('thisYear').logic(nextYear) &&
!fdt.condition('thisYear').logic(null))
.toBeTruthy('thisYear');
expect(fdt.condition('lastYear').logic(lastYear) &&
!fdt.condition('lastYear').logic(now) &&
!fdt.condition('lastYear').logic(null))
.toBeTruthy('lastYear');
expect(fdt.condition('nextYear').logic(nextYear) &&
!fdt.condition('nextYear').logic(now) &&
!fdt.condition('nextYear').logic(null))
.toBeTruthy('nextYear');
} finally {
jasmine.clock().uninstall();
}
});
it('tests dateTime conditions when the current month rolls over a year boundary', () => {
const fdt = IgxDateTimeFilteringOperand.instance();
const fd = IgxDateFilteringOperand.instance();
jasmine.clock().install();
try {
// In January `lastMonth` has to roll back to December of the previous year.
jasmine.clock().mockDate(new Date(2024, 0, 15));
expect(fdt.condition('lastMonth').logic(new Date(2023, 11, 31)) &&
!fdt.condition('lastMonth').logic(new Date(2024, 0, 1)) &&
!fdt.condition('lastMonth').logic(new Date(2023, 10, 30)))
.toBeTruthy('dateTime lastMonth in January');
expect(fd.condition('lastMonth').logic(new Date(2023, 11, 31)) &&
!fd.condition('lastMonth').logic(new Date(2024, 0, 1)))
.toBeTruthy('date lastMonth in January');
expect(fdt.condition('nextMonth').logic(new Date(2024, 1, 1)) &&
!fdt.condition('nextMonth').logic(new Date(2024, 0, 31)))
.toBeTruthy('dateTime nextMonth in January');

// In December `nextMonth` has to roll forward to January of the next year.
jasmine.clock().mockDate(new Date(2024, 11, 15));
expect(fdt.condition('nextMonth').logic(new Date(2025, 0, 1)) &&
!fdt.condition('nextMonth').logic(new Date(2024, 11, 31)) &&
!fdt.condition('nextMonth').logic(new Date(2026, 0, 1)))
.toBeTruthy('dateTime nextMonth in December');
expect(fd.condition('nextMonth').logic(new Date(2025, 0, 1)) &&
!fd.condition('nextMonth').logic(new Date(2024, 11, 31)))
.toBeTruthy('date nextMonth in December');
expect(fdt.condition('lastMonth').logic(new Date(2024, 10, 30)) &&
!fdt.condition('lastMonth').logic(new Date(2024, 11, 1)))
.toBeTruthy('dateTime lastMonth in December');
} finally {
jasmine.clock().uninstall();
}
});
it('tests time conditions', () => {
const ft = IgxTimeFilteringOperand.instance();
const at = new Date(2024, 4, 17, 10, 30, 30);
const earlierHours = new Date(2024, 4, 17, 9, 30, 30);
const earlierMinutes = new Date(2024, 4, 17, 10, 15, 30);
const earlierSeconds = new Date(2024, 4, 17, 10, 30, 15);
const laterHours = new Date(2024, 4, 17, 11, 30, 30);
const laterMinutes = new Date(2024, 4, 17, 10, 45, 30);
const laterSeconds = new Date(2024, 4, 17, 10, 30, 45);
// The date part is deliberately different - only the time part participates.
const sameTimeOtherDay = new Date(2020, 0, 1, 10, 30, 30);

expect(ft.condition('at_before').logic(at, at) &&
ft.condition('at_before').logic(sameTimeOtherDay, at) &&
ft.condition('at_before').logic(earlierHours, at) &&
ft.condition('at_before').logic(earlierMinutes, at) &&
ft.condition('at_before').logic(earlierSeconds, at))
.toBeTruthy('at_before matches the exact time and everything before it');
expect(!ft.condition('at_before').logic(laterHours, at) &&
!ft.condition('at_before').logic(laterMinutes, at) &&
!ft.condition('at_before').logic(laterSeconds, at) &&
!ft.condition('at_before').logic(null, at))
.toBeTruthy('at_before does not match later times');

expect(ft.condition('at_after').logic(at, at) &&
ft.condition('at_after').logic(sameTimeOtherDay, at) &&
ft.condition('at_after').logic(laterHours, at) &&
ft.condition('at_after').logic(laterMinutes, at) &&
ft.condition('at_after').logic(laterSeconds, at))
.toBeTruthy('at_after matches the exact time and everything after it');
expect(!ft.condition('at_after').logic(earlierHours, at) &&
!ft.condition('at_after').logic(earlierMinutes, at) &&
!ft.condition('at_after').logic(earlierSeconds, at) &&
!ft.condition('at_after').logic(null, at))
.toBeTruthy('at_after does not match earlier times');

// `in` matches on the locale time string, so the date part is irrelevant here as well.
const times = new Set([at.toLocaleTimeString()]);
expect(ft.condition('in').logic(at, times) &&
ft.condition('in').logic(sameTimeOtherDay, times) &&
!ft.condition('in').logic(laterHours, times) &&
!ft.condition('in').logic(null, times))
.toBeTruthy('in');
});
it('tests the shared date-time helpers', () => {
const date = new Date(2024, 4, 17, 13, 24, 35, 678);

// Without a date, or without a format, every part stays null.
const noDate = IgxDateFilteringOperand.getDateParts(null, 'yMdhmsf');
const noFormat = IgxDateFilteringOperand.getDateParts(date);
expect(Object.values(noDate).every(part => part === null)).toBeTruthy('no date');
expect(Object.values(noFormat).every(part => part === null)).toBeTruthy('no format');

// Each part is resolved only when the format asks for it.
expect(IgxDateFilteringOperand.getDateParts(date, 'yMdhmsf')).toEqual({
year: 2024, month: 4, day: 17, hours: 13, minutes: 24, seconds: 35, milliseconds: 678
});
const dateOnly = IgxDateFilteringOperand.getDateParts(date, 'yMd');
expect(dateOnly.hours === null && dateOnly.minutes === null &&
dateOnly.seconds === null && dateOnly.milliseconds === null)
.toBeTruthy('parts outside of the format stay null');

// Each operand matches against the set through its own string form - the full ISO string for
// dateTime, the date part only for date, and the locale time for time.
const fdt = IgxDateTimeFilteringOperand.instance();
expect(fdt.condition('in').logic(date, new Set([date.toISOString()])) &&
!fdt.condition('in').logic(date, new Set([new Date(2020, 0, 1).toISOString()])) &&
fdt.condition('in').logic('plain value', new Set(['plain value'])) &&
!fdt.condition('in').logic(null, new Set([date.toISOString()])))
.toBeTruthy('dateTime in');

const fd = IgxDateFilteringOperand.instance();
expect(fd.condition('in').logic(date, new Set([date.toDateString()])) &&
!fd.condition('in').logic(date, new Set([new Date(2020, 0, 1).toDateString()])) &&
!fd.condition('in').logic(null, new Set([date.toDateString()])))
.toBeTruthy('date in');

expect(() => fd.condition('before').logic('not a date', date))
.toThrowError(
'Could not perform filtering on \'date\' column because the datasource object type is not \'Date\'.');
});
it('tests nested query conditions', () => {
const f = IgxStringFilteringOperand.instance();
const values = new Set(['a', 'b']);

expect(f.condition('inQuery').logic('a', values) && !f.condition('inQuery').logic('c', values))
.toBeTruthy('inQuery');
expect(f.condition('notInQuery').logic('c', values) && !f.condition('notInQuery').logic('a', values))
.toBeTruthy('notInQuery');
// Nested query conditions are hidden from the plain condition list, but not from the extended one.
expect(f.conditionList()).not.toContain('inQuery');
expect(f.extendedConditionList()).toContain('inQuery');
expect(f.extendedConditionList()).toContain('notInQuery');
});
it('tests custom conditions', () => {
const f = CustomFilter.instance();
expect(f.condition('Custom').logic('Asd', 'asd')).toBeFalsy();
Expand Down
Loading
Loading