Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,188 @@ 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();
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');
});
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