Last active
September 8, 2024 06:11
-
-
Save drewwiens/8e6e33f67b0adbee3ff6468e3221c71a to your computer and use it in GitHub Desktop.
Handy custom RxJS operators for Angular etc
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { AbstractControl, FormArray, FormGroup } from '@angular/forms'; | |
import { map, toPairs, fromPairs, differenceWith, isEqual, isNull, isUndefined } from 'lodash'; | |
import { Observable, OperatorFunction, defer, empty, of, merge, pipe } from 'rxjs'; | |
import { distinctUntilChanged, filter, map, shareReplay, pairwise } from 'rxjs/operators'; | |
/** | |
* Convenience RxJS operator that filters out undefined & null and modifies the downstream type | |
* appropriately. | |
*/ | |
export function exists<T>(): OperatorFunction<T | undefined | null, T> { | |
return filter( | |
(v: T | undefined | null): v is T => !isUndefined(v) && !isNull(v), | |
); | |
} | |
/** | |
* Get an observable for the latest value of a control including its initial value. | |
* | |
* @param source The AbstractControl itself or its ancestor in the hierarchy. | |
* @param path If included, the path from source to the control. If not included, then source is | |
* used directly. | |
*/ | |
export function toValueOfControl<T>( | |
source: AbstractControl, | |
path?: string | (string | number)[], | |
): Observable<T> { | |
const control = path ? source.get(path) : source; | |
if (!control) { | |
return empty(); | |
} | |
// startWith evaluates immediately, but we need to get the value of the control at subscription | |
// time. Deferring allows us to get the updated value. | |
return merge(control.valueChanges, defer(() => of(control.value))).pipe( | |
distinctUntilChanged(isEqual), | |
shareReplay(1), | |
); | |
} | |
/** | |
* Get an observable for the latest raw value of a control including its initial raw value. | |
* | |
* @param source The AbstractControl itself or its ancestor in the hierarchy. | |
* @param path If included, the path from source to the control. If not included, then source is | |
* used directly. | |
*/ | |
export function toRawValueOfControl<T>( | |
source: AbstractControl, | |
path?: string | (string | number)[], | |
): Observable<T> { | |
const control = path ? source.get(path) : source; | |
if (!control) { | |
return empty(); | |
} | |
if (isFormGroup(control) || isFormArray(control)) { | |
// startWith evaluates immediately, but we need to get the value of the control at | |
// subscription time. Deferring allows us to get the updated value. | |
return merge(control.valueChanges, defer(() => of(control.getRawValue()))).pipe( | |
// get the raw value that contains all of the children, even the disabled ones | |
map(_value => control.getRawValue()), | |
distinctUntilChanged(isEqual), | |
shareReplay(1), | |
); | |
} else { | |
// a FormControl doesn't distinguish between value and rawValue, so just use the other function. | |
return toValueOfControl(control); | |
} | |
} | |
/** | |
* An RxJS operator that emits true if the source string starts with any of the given prefixes. | |
* @param prefixes Array of string prefixes. | |
*/ | |
export function startsWith(prefixes: string[]) { | |
return map<string, boolean>(type => prefixes.some(pre => type.startsWith(pre))); | |
} | |
function isFormGroup(control: AbstractControl): control is FormGroup { | |
return control instanceof FormGroup; | |
} | |
function isFormArray(control: AbstractControl): control is FormArray { | |
return control instanceof FormArray; | |
} | |
/** | |
* An RxJS operator for the common shareReplay use-case where | |
* refCount = true and bufferSize = 1. Equivalent to shareReplay(1) in older | |
* versions of rxjs. | |
*/ | |
export function shareReplay1<T>() { | |
return shareReplay<T>({ refCount: true, bufferSize: 1 }); | |
} | |
/** | |
* RxJS operator that emits an object containing only the key-value pairs that changed. Does not | |
* emit anything until at least two values have been received. | |
*/ | |
export function diff<T extends Record<string, any>>(): OperatorFunction<T, Partial<T>> { | |
return pipe( | |
map(toPairs), | |
pairwise(), | |
map(([prev, next]) => fromPairs(differenceWith(next, prev, isEqual)) as Partial<T>), | |
); | |
} | |
import { isFunction, isNull, isUndefined } from 'lodash'; | |
import { EMPTY, Observable, of, OperatorFunction } from 'rxjs'; | |
import { filter, map, shareReplay, take, timeoutWith } from 'rxjs/operators'; | |
/** | |
* Generic assertion function to prove that a value is truthy. | |
* | |
* @param value The value to check | |
* @param message The error message to display | |
*/ | |
export function truthy<T>( | |
value: T | undefined | null, | |
message = 'value is falsy', | |
): asserts value is T { | |
if (!value) { | |
throw new Error(message); | |
} | |
} | |
/** | |
* Generic assertion function to prove that a value is truthy. | |
* | |
* @param value The value to check | |
* @param message The error message to display | |
*/ | |
export function assertFunction( | |
// eslint-disable-next-line @typescript-eslint/no-explicit-any | |
value: any, | |
message = 'value is not a function', | |
): asserts value is CallableFunction { | |
if (!isFunction(value)) { | |
throw new Error(message); | |
} | |
} | |
/** | |
* asserts the value is truthy as to return it for chaining | |
*/ | |
export function assure<T>(value: T | undefined | null): T { | |
truthy(value); | |
return value; | |
} | |
/** | |
* An RxJS operator for the common shareReplay use-case where | |
* refCount = true and bufferSize = 1. | |
*/ | |
export function shareReplay1<T>() { | |
return shareReplay<T>({ refCount: true, bufferSize: 1 }); | |
} | |
/** | |
* Get one item from source observable as a Promise. | |
* | |
* @param obs$ Source observable. | |
*/ | |
export function resolveOne<T>(obs$: Observable<T>): Promise<T> { | |
return obs$.pipe(take(1)).toPromise(); | |
} | |
/** | |
* Get current value from source observable as a Promise that resolves on the | |
* next tick. | |
* | |
* Note this can only get the "current value" of the observable if the | |
* observable emits immediately, e.g. BehaviorSubject (no other subscribers | |
* needed) or any observable piped thru shareReplay1() operator that has at | |
* least one other active subscription. | |
* | |
* Resolves to rxjs's EMPTY object if nothing was emitted, i.e. you can check | |
* if nothing was emitted by checking if the resolved value === EMPTY. This is | |
* not the same as resolving to EMPTY, which would resolve to undefined and | |
* would be indistinguishable from an observable that has a value of undefined. | |
* | |
* @param obs$ Source observable. | |
*/ | |
export function valueOf<T>(obs$: Observable<T>): Promise<T | typeof EMPTY> { | |
return obs$.pipe(take(1), timeoutWith(0, of(EMPTY))).toPromise(); | |
} | |
/** | |
* Convenience RxJS operator that filters out undefined & null and modifies the | |
* downstream type appropriately. | |
*/ | |
export function exists<T>(): OperatorFunction<T | undefined | null, T> { | |
return filter( | |
(v: T | undefined | null): v is T => !isUndefined(v) && !isNull(v), | |
); | |
} | |
/** RxJS pipeable operator that emits true if all array items are truthy. */ | |
export const allTruthy = () => | |
// eslint-disable-next-line @typescript-eslint/no-explicit-any | |
map((array: any[]) => array.every(Boolean)); | |
/** RxJS pipeable operator that emits true if any array items are truthy. */ | |
export const someTruthy = () => | |
// eslint-disable-next-line @typescript-eslint/no-explicit-any | |
map((array: any[]) => array.some(Boolean)); | |
/** | |
* Convert a scalar, array, or undefined value to an array of that type. If the | |
* input is an array, then just use that. If it's a scalar, then wrap it in an | |
* array. If it's falsy, then replace with an empty array. | |
* | |
* @param input scalar, array, or undefined | |
*/ | |
export function wrapInArray<T>(input?: T | T[]): T[] { | |
if (input === undefined) { | |
return []; | |
} | |
return Array.isArray(input) ? [...input] : [input]; | |
} | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Tests for some operators in rxjs-operators.ts are contained in this file | |
/* eslint-disable rxjs/no-sharereplay */ | |
import { subYears } from 'date-fns'; | |
import { isEqual } from 'lodash'; | |
import { firstValueFrom, from, of } from 'rxjs'; | |
import { shareReplay, toArray, distinctUntilChanged } from 'rxjs/operators'; | |
import { | |
allTruthy, | |
assertPropsNotNil, | |
assertString, | |
assureNotNil, | |
assureNotNull, | |
assurePropsNotNil, | |
convertNullProps, | |
convertToTimestamp, | |
distinctUntilNotEqual, | |
exists, | |
hash, | |
isBlankString, | |
isEmptyOrNil, | |
isNotNil, | |
isNotNull, | |
notNil, | |
notNull, | |
parseNumber, | |
resolveMany, | |
shareReplay1, | |
someTruthy, | |
toBoolean, | |
truthy, | |
urlJoin, | |
wildcardSearch, | |
wrapInArray, | |
extractFilenameFromFullPath, | |
caseInsensitiveIncludes, | |
caseInsensitiveEquals, | |
getLastYearRange, | |
escapeSpecialChars, | |
parseJson, | |
} from './functions'; | |
jest.mock('rxjs/operators', () => ({ | |
...jest.requireActual('rxjs/operators'), | |
shareReplay: jest.fn(() => 'mockShareReplay'), | |
distinctUntilChanged: jest.fn(() => 'mockDistinctUntilChanged'), | |
})); | |
describe('lang functions', () => { | |
beforeEach(() => { | |
jest.clearAllMocks(); | |
}); | |
describe(isBlankString.name, () => { | |
it('should handle true cases', () => { | |
expect(isBlankString('')).toBe(true); | |
expect(isBlankString(' ')).toBe(true); | |
}); | |
it('should handle false cases for string', () => { | |
expect(isBlankString('abc')).toBe(false); | |
}); | |
it('should handle none string cases', () => { | |
expect(isBlankString(0)).toBe(false); | |
expect(isBlankString(false)).toBe(false); | |
}); | |
}); | |
describe(isEmptyOrNil.name, () => { | |
it('should handle true cases', () => { | |
expect(isEmptyOrNil(' ')).toBe(true); | |
expect(isEmptyOrNil('')).toBe(true); | |
expect(isEmptyOrNil(undefined)).toBe(true); | |
expect(isEmptyOrNil(null)).toBe(true); | |
}); | |
it('should handle false cases', () => { | |
expect(isEmptyOrNil('0')).toBe(false); | |
expect(isEmptyOrNil('abc')).toBe(false); | |
}); | |
}); | |
describe('notNull', () => { | |
it('should assert if a non-null value is passed in', () => { | |
[0, 1, 'hello', {}, [], [1], true, undefined].forEach((value) => { | |
try { | |
notNull(value); | |
} catch (e) { | |
fail('do not expect to get here'); | |
} | |
}); | |
}); | |
it('should throw an error if a null value is passed in', () => { | |
try { | |
notNull(null); | |
fail('do not expect to get here'); | |
} catch (e) { | |
expect(e).toBeTruthy(); | |
} | |
}); | |
}); | |
describe('assureNotNull', () => { | |
it('should assert if a non-null value is passed in', () => { | |
[0, 1, 'hello', {}, [], [1], true, undefined].forEach((value) => { | |
try { | |
expect(assureNotNull(value)).toBe(value); | |
} catch (e) { | |
fail('do not expect to get here'); | |
} | |
}); | |
}); | |
it('should throw an error if a null value is passed in', () => { | |
try { | |
assureNotNull(null); | |
fail('do not expect to get here'); | |
} catch (e) { | |
expect(e).toBeTruthy(); | |
} | |
}); | |
}); | |
describe('notNil', () => { | |
it('should assert if a non-nil value is passed in', () => { | |
[0, 1, 'hello', {}, [], [1], true].forEach((value) => { | |
try { | |
notNil(value); | |
} catch (e) { | |
fail('do not expect to get here'); | |
} | |
}); | |
}); | |
it('should throw an error if a nil value is passed in', () => { | |
[undefined, null].forEach((value) => { | |
try { | |
notNil(value); | |
fail('do not expect to get here'); | |
} catch (e) { | |
expect(e).toBeTruthy(); | |
} | |
}); | |
}); | |
}); | |
describe('assureNotNil', () => { | |
it('should assert if a non-nil value is passed in', () => { | |
[0, 1, 'hello', {}, [], [1], true].forEach((value) => { | |
try { | |
expect(assureNotNil(value)).toBe(value); | |
} catch (e) { | |
fail('do not expect to get here'); | |
} | |
}); | |
}); | |
it('should throw an error if a nil value is passed in', () => { | |
[undefined, null].forEach((value) => { | |
try { | |
assureNotNil(value); | |
fail('do not expect to get here'); | |
} catch (e) { | |
expect(e).toBeTruthy(); | |
} | |
}); | |
}); | |
}); | |
describe('truthy', () => { | |
it('should assert if a truthy value is passed in', () => { | |
[1, 'hello', {}, [], [1], true].forEach((value) => { | |
try { | |
truthy(value); | |
} catch (e) { | |
fail('do not expect to get here'); | |
} | |
}); | |
}); | |
it('should throw an error if a falsy value is passed in', () => { | |
[0, '', undefined, NaN, false, null].forEach((value) => { | |
try { | |
truthy(value); | |
fail('do not expect to get here'); | |
} catch (e) { | |
expect(e).toBeTruthy(); | |
} | |
}); | |
}); | |
}); | |
describe('convertNullProps', () => { | |
it('should convert all nulls to undefined', () => { | |
const obj = { a: null, b: 0, c: undefined }; | |
expect(convertNullProps(obj)).toEqual({ | |
a: undefined, | |
b: 0, | |
c: undefined, | |
}); | |
}); | |
it('should convert all nulls to undefined for a frozen object', () => { | |
const obj = Object.freeze({ a: null, b: 0, c: undefined }); | |
expect(convertNullProps(obj)).toEqual({ | |
a: undefined, | |
b: 0, | |
c: undefined, | |
}); | |
}); | |
}); | |
describe('assertPropsNotNil', () => { | |
it('should assert if a correct value is passed in', () => { | |
[{ f: 0 }, { f: 3 }, { f: 'x' }, { f: {} }, {}].forEach((value) => { | |
expect(() => assertPropsNotNil(value)).not.toThrow(); | |
}); | |
}); | |
it('should throw an error if an incorrect value is passed in', () => { | |
[{ f: null }, { f: undefined }].forEach((value) => { | |
expect(() => | |
assertPropsNotNil(value, (f, v) => `field ${f} is ${v}`), | |
).toThrow('field f is'); | |
}); | |
}); | |
}); | |
describe('assertString', () => { | |
it('should assert if a string is passed in', () => { | |
expect(() => assertString('')).not.toThrow(); | |
}); | |
it('should throw an error if an incorrect value is passed in', () => { | |
[0, {}, null, undefined, Symbol(), []].forEach((value) => { | |
expect(() => assertString(value)).toThrow( | |
new RegExp(`string.+${typeof value}`, 'i'), | |
); | |
}); | |
}); | |
}); | |
describe('assurePropsNotNil', () => { | |
it('should assert if a correct value is passed in', () => { | |
[{ f: 0 }, { f: 3 }, { f: 'x' }, { f: {} }, {}].forEach((value) => { | |
expect(assurePropsNotNil(value)).toBe(value); | |
}); | |
}); | |
it('should throw an error if an incorrect value is passed in', () => { | |
[{ f: null }, { f: undefined }].forEach((value) => { | |
expect(() => assurePropsNotNil(value)).toThrow(); | |
}); | |
}); | |
}); | |
describe('isNotNull', () => { | |
it('should return not isNull', () => { | |
expect(isNotNull(null)).toBe(false); | |
expect(isNotNull(undefined)).toBe(true); | |
expect(isNotNull(0)).toBe(true); | |
expect(isNotNull(NaN)).toBe(true); | |
expect(isNotNull('')).toBe(true); | |
expect(isNotNull(false)).toBe(true); | |
}); | |
}); | |
describe('isNotNil', () => { | |
it('should return not isNil', () => { | |
expect(isNotNil(null)).toBe(false); | |
expect(isNotNil(undefined)).toBe(false); | |
expect(isNotNil(0)).toBe(true); | |
expect(isNotNil(NaN)).toBe(true); | |
expect(isNotNil('')).toBe(true); | |
expect(isNotNil(false)).toBe(true); | |
}); | |
}); | |
describe('shareReplay1', () => { | |
it('should return shareReplay with correct options', () => { | |
const result = shareReplay1(); | |
expect(shareReplay).toHaveBeenCalledWith({ | |
refCount: true, | |
bufferSize: 1, | |
}); | |
expect(result).toBe('mockShareReplay'); | |
}); | |
}); | |
describe('resolveMany', () => { | |
it('should emit an array', async () => { | |
const obs$ = of(1, 2, 3); | |
const output = await resolveMany(obs$, 3); | |
expect(output).toEqual([1, 2, 3]); | |
}); | |
}); | |
describe('exists', () => { | |
it('should filter undefined and null', async () => { | |
const array = [1, undefined, null, 2]; | |
const result = await firstValueFrom( | |
from(array).pipe(exists(), toArray()), | |
); | |
expect(result).toEqual([1, 2]); | |
}); | |
}); | |
describe('allTruthy', () => { | |
it('should return array.every(Boolean)', async () => { | |
const mock = { every: jest.fn(() => 'every') }; | |
const source$ = of(mock as any).pipe(allTruthy()); | |
expect(await firstValueFrom(source$)).toBe('every'); | |
expect(mock.every).toHaveBeenCalledWith(Boolean); | |
}); | |
}); | |
describe('someTruthy', () => { | |
it('should return array.some(Boolean)', async () => { | |
const mock = { some: jest.fn(() => 'some') }; | |
const source$ = of(mock as any).pipe(someTruthy()); | |
expect(await firstValueFrom(source$)).toBe('some'); | |
expect(mock.some).toHaveBeenCalledWith(Boolean); | |
}); | |
}); | |
describe('wrapInArray', () => { | |
it('should handle undefined', () => { | |
expect(wrapInArray(undefined)).toEqual([]); | |
}); | |
it('should handle an empty array', () => { | |
expect(wrapInArray([])).toEqual([]); | |
}); | |
it('should handle an array', () => { | |
expect(wrapInArray(['a', 'b', 'c'])).toEqual(['a', 'b', 'c']); | |
}); | |
it('should handle an array or arrays', () => { | |
expect(wrapInArray([['a', 'b'], ['c']])).toEqual([['a', 'b'], ['c']]); | |
}); | |
it('should handle a truthy scalar', () => { | |
expect(wrapInArray('a')).toEqual(['a']); | |
expect(wrapInArray(1)).toEqual([1]); | |
expect(wrapInArray(true)).toEqual([true]); | |
}); | |
it('should handle a falsy scalar', () => { | |
expect(wrapInArray('')).toEqual(['']); | |
expect(wrapInArray(0)).toEqual([0]); | |
expect(wrapInArray(false)).toEqual([false]); | |
expect(wrapInArray(null)).toEqual([null]); | |
}); | |
}); | |
describe('hash', () => { | |
it('should produce different hash', () => { | |
const hello = hash('hello world'); | |
const test = hash('test 123'); | |
expect(hello).not.toEqual(test); | |
}); | |
}); | |
describe('parseNumber', () => { | |
it('should parse valid numbers', () => { | |
expect(parseNumber('5')).toBe(5); | |
expect(parseNumber('8.2')).toBe(8.2); | |
expect(parseNumber('3e7')).toBe(3e7); | |
}); | |
it('should ignore whitespace', () => { | |
expect(parseNumber(' 8')).toBe(8); | |
expect(parseNumber(' \n 8 \n ')).toBe(8); | |
}); | |
it('should parse other bases', () => { | |
expect(parseNumber('0x34ab')).toBe(0x34ab); | |
expect(parseNumber('0b110100')).toBe(0b110100); | |
expect(parseNumber('0o1246')).toBe(0o1246); | |
}); | |
it('should parse NaN and infinities', () => { | |
expect(parseNumber('Infinity')).toBe(Infinity); | |
expect(parseNumber('-Infinity')).toBe(-Infinity); | |
expect(parseNumber('NaN')).toBe(NaN); | |
}); | |
it('should fail on non digits', () => { | |
expect(() => parseNumber('octopus')).toThrow(); | |
expect(() => parseNumber('a67')).toThrow(); | |
expect(() => parseNumber('67a')).toThrow(); | |
}); | |
}); | |
describe(toBoolean.name, () => { | |
it('should handle "true"', () => { | |
expect(toBoolean('true')).toBe(true); | |
}); | |
it('should handle " true "', () => { | |
expect(toBoolean(' true ')).toBe(true); | |
}); | |
it('should handle "TRUE"', () => { | |
expect(toBoolean('TRUE')).toBe(true); | |
}); | |
it('should handle "false"', () => { | |
expect(toBoolean('false')).toBe(false); | |
}); | |
it('should handle "FALSE"', () => { | |
expect(toBoolean('FALSE')).toBe(false); | |
}); | |
it('should handle missing', () => { | |
expect(toBoolean(undefined)).toBe(false); | |
}); | |
}); | |
describe(wildcardSearch.name, () => { | |
it('should make wildcard search', () => { | |
expect(wildcardSearch('m*', 'Mac')).toBe(true); | |
expect(wildcardSearch('*c', 'Mac')).toBe(true); | |
expect(wildcardSearch('*a*', 'Mac')).toBe(true); | |
expect(wildcardSearch('*m', 'Mac')).toBe(false); | |
expect(wildcardSearch('*x*', 'Mac')).toBe(false); | |
}); | |
it('should make a regular search', () => { | |
expect(wildcardSearch('m', 'Mac')).toBe(true); | |
expect(wildcardSearch('x', 'Mac')).toBe(false); | |
}); | |
}); | |
describe(convertToTimestamp.name, () => { | |
it('should convert date string to timestamp in ms', () => { | |
const ms = convertToTimestamp('2021-12-31T23:00:00-0200'); | |
expect(ms).toEqual(1640998800000); | |
expect(new Date(ms).toISOString()).toEqual('2022-01-01T01:00:00.000Z'); | |
}); | |
}); | |
describe('urlJoin', () => { | |
it('should handle leading slash', () => { | |
expect(urlJoin('/foo', 'bar')).toBe('/foo/bar'); | |
}); | |
it('should handle no leading slash', () => { | |
expect(urlJoin('foo', 'bar')).toBe('foo/bar'); | |
}); | |
it('should handle "/" before a leading slash', () => { | |
expect(urlJoin('/', '/foo', 'bar')).toBe('/foo/bar'); | |
}); | |
// Test case from url-join-ts, (C) Alexey Nikitin, MIT license: | |
it.each([['http'], ['https']])('urlJoin with %s protocol', (protocol) => { | |
const baseUrl = `${protocol}://test.com`; | |
expect(urlJoin(baseUrl)).toBe(`${baseUrl}`); | |
expect(urlJoin(baseUrl, null)).toBe(`${baseUrl}`); | |
expect(urlJoin(baseUrl, undefined)).toBe(`${baseUrl}`); | |
expect(urlJoin(baseUrl, null, '1')).toBe(`${baseUrl}/1`); | |
expect(urlJoin(baseUrl, undefined, '1')).toBe(`${baseUrl}/1`); | |
expect(urlJoin(baseUrl, '')).toBe(`${baseUrl}`); | |
expect(urlJoin(baseUrl, '1')).toBe(`${baseUrl}/1`); | |
expect(urlJoin(baseUrl, '1/')).toEqual(`${baseUrl}/1`); | |
expect(urlJoin(baseUrl, '/1')).toBe(`${baseUrl}/1`); | |
expect(urlJoin(baseUrl, '1', '2')).toBe(`${baseUrl}/1/2`); | |
expect(urlJoin(baseUrl, '1/2')).toBe(`${baseUrl}/1/2`); | |
}); | |
// Test case from url-join-ts, (C) Alexey Nikitin, MIT license: | |
it('urlJoin for path', () => { | |
expect(urlJoin(undefined, '')).toBe(``); | |
expect(urlJoin(undefined, '1')).toBe(`1`); | |
expect(urlJoin(undefined, '/1')).toBe(`/1`); | |
expect(urlJoin(undefined, '1', '/2/')).toBe(`1/2`); | |
expect(urlJoin(undefined, '/1', '/2/')).toBe(`/1/2`); | |
expect(urlJoin(undefined, '1', '/2', '3')).toBe(`1/2/3`); | |
expect(urlJoin(undefined, '1', '/2', '/3/4/5/')).toBe(`1/2/3/4/5`); | |
}); | |
// Test case from url-join-ts, (C) Alexey Nikitin, MIT license: | |
it('urlJoin for localhost', () => { | |
expect(urlJoin('http://localhost', '1')).toBe(`http://localhost/1`); | |
expect(urlJoin('http://localhost', '/1')).toBe(`http://localhost/1`); | |
expect(urlJoin('localhost', '1')).toBe(`localhost/1`); | |
expect(urlJoin('http://0.0.0.0', '1')).toBe(`http://0.0.0.0/1`); | |
expect(urlJoin('http://127.0.0.1', '1')).toBe(`http://127.0.0.1/1`); | |
}); | |
}); | |
describe('distinctUntilNotEqual', () => { | |
it('should return distinctUntilChanged(isEqual)', () => { | |
// Arrange | |
// Act | |
const result = distinctUntilNotEqual(); | |
// Assert | |
expect(result).toBe('mockDistinctUntilChanged'); | |
expect(distinctUntilChanged).toHaveBeenCalledTimes(1); | |
expect(distinctUntilChanged).toHaveBeenCalledWith(isEqual); | |
}); | |
}); | |
describe(extractFilenameFromFullPath.name, () => { | |
it('should return file name', () => { | |
expect(extractFilenameFromFullPath('/folder1/folder2/filename.txt')).toBe( | |
'filename.txt', | |
); | |
}); | |
}); | |
describe(caseInsensitiveIncludes.name, () => { | |
it('should handle happy case', () => { | |
expect(['Foo', 'bar'].filter(caseInsensitiveIncludes('foo'))).toEqual([ | |
'Foo', | |
]); | |
}); | |
}); | |
describe(caseInsensitiveEquals.name, () => { | |
it('should handle happy case', () => { | |
expect(['Foo', 'bar'].some(caseInsensitiveEquals('foo'))).toEqual(true); | |
}); | |
}); | |
describe(getLastYearRange.name, () => { | |
it('should handle the happy case', () => { | |
// Arrange | |
const today = new Date(); | |
const endDate = new Date( | |
today.getFullYear(), | |
today.getMonth(), | |
today.getDate(), | |
); | |
const startDate = subYears(endDate, 1); | |
// Act | |
const result = getLastYearRange(); | |
// Assert | |
expect(result).toEqual({ | |
startDate, | |
endDate, | |
}); | |
}); | |
}); | |
describe(escapeSpecialChars.name, () => { | |
it('should handle the happy case', () => { | |
// Act | |
const result = escapeSpecialChars('So special! +<>#.*+?^${}()|/'); | |
// Assert | |
expect(result).toEqual( | |
'So special\\! \\+\\<\\>\\#\\.\\*\\+\\?\\^\\$\\{\\}\\(\\)\\|\\/', | |
); | |
}); | |
}); | |
describe(parseJson.name, () => { | |
it('should handle the happy case', () => { | |
// Act | |
const result = parseJson('{"foo":"bar"}'); | |
// Assert | |
expect(result).toEqual({ foo: 'bar' }); | |
}); | |
it('should handle the error case', () => { | |
// Act | |
const result = parseJson('foo'); | |
// Assert | |
expect(result).toEqual(undefined); | |
}); | |
}); | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment