-
Notifications
You must be signed in to change notification settings - Fork 16
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
correctly use defineProperties and defineProperty, add required, tests
- Loading branch information
1 parent
11c4ad9
commit d501174
Showing
4 changed files
with
332 additions
and
188 deletions.
There are no files selected for viewing
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,110 +1,211 @@ | ||
import { assertType, describe, expect, it } from 'vitest'; | ||
/* eslint-disable no-restricted-syntax */ | ||
|
||
import { describe, expect, expectTypeOf, it, test } from 'vitest'; | ||
import { createGetter } from './create-getter.js'; | ||
import { Getter } from './getter.js'; | ||
|
||
type City = 'Seattle' | 'San Francisco' | 'New York City'; | ||
|
||
interface Address { | ||
city: string; | ||
state: string; | ||
country?: string; | ||
city: City; | ||
state?: string; | ||
} | ||
|
||
interface Employee { | ||
interface Person { | ||
firstName: string; | ||
lastName?: string; | ||
address?: Address; | ||
} | ||
|
||
const employee: Employee = { | ||
const alice: Person = { | ||
firstName: 'Alice', | ||
lastName: 'Liddell', | ||
address: { | ||
city: 'San Francisco', | ||
state: 'California', | ||
}, | ||
}; | ||
|
||
const getFirstName = createGetter((employee?: Employee) => employee?.firstName); | ||
const getLastName = createGetter((employee?: Employee) => employee?.lastName); | ||
const getAddress = createGetter((employee?: Employee) => employee?.address); | ||
const getCity = createGetter((address?: Address) => address?.city); | ||
const getCountry = createGetter((address?: Address) => address?.country); | ||
const getFirstLetter = createGetter((value?: string) => value?.[0]); | ||
const bob: Person = { | ||
firstName: 'Bob', | ||
address: { | ||
city: 'Seattle', | ||
}, | ||
}; | ||
|
||
const charlie: Person = { | ||
firstName: 'Charlie', | ||
lastName: '', | ||
}; | ||
|
||
describe('createGetter()', () => { | ||
describe('getter()', () => { | ||
it('gets the value via the function passed into `createGetter()`', () => { | ||
expect(getFirstName(employee)).toBe('Alice'); | ||
}); | ||
const selectFirstName = (p?: Person) => p?.firstName; | ||
const selectStringIndexOne = (a?: string) => a?.[1]; | ||
|
||
it('creates a getter', () => { | ||
let getFirstNameFromPerson; | ||
expectTypeOf((getFirstNameFromPerson = createGetter(selectFirstName))).toEqualTypeOf< | ||
Getter<Person, string> | ||
>(); | ||
expect(getFirstNameFromPerson).toBeInstanceOf(Function); | ||
expect(getFirstNameFromPerson).toHaveProperty(['optional', 'pipe']); | ||
}); | ||
|
||
it('creates a getter with optional', () => { | ||
let getFirstNameFromPerson_optional; | ||
expectTypeOf( | ||
(getFirstNameFromPerson_optional = createGetter(selectFirstName).optional), | ||
).toEqualTypeOf<Getter<Person, string | undefined>>(); | ||
|
||
expect(getFirstNameFromPerson_optional).toBeInstanceOf(Function); | ||
expect(getFirstNameFromPerson_optional).toHaveProperty('optional'); | ||
expect(getFirstNameFromPerson_optional).toHaveProperty('pipe'); | ||
|
||
expect(getFirstNameFromPerson_optional.optional).toBe(getFirstNameFromPerson_optional); | ||
}); | ||
|
||
it('creates a getter pipe', () => { | ||
const getFirstName = createGetter(selectFirstName); | ||
const getSecondLetter = createGetter(selectStringIndexOne); | ||
|
||
let getSecondLetterOfFirstNameFromPerson; | ||
expectTypeOf( | ||
(getSecondLetterOfFirstNameFromPerson = getFirstName.pipe(getSecondLetter)), | ||
).toEqualTypeOf<Getter<Person, string>>(); | ||
|
||
expect(getSecondLetterOfFirstNameFromPerson).toBeInstanceOf(Function); | ||
expect(getSecondLetterOfFirstNameFromPerson).toHaveProperty('optional'); | ||
expect(getSecondLetterOfFirstNameFromPerson).toHaveProperty('pipe'); | ||
}); | ||
}); | ||
|
||
it('throws when the whole value is undefined', () => { | ||
describe('getting values and optional', () => { | ||
const getFirstName = createGetter((p?: Person) => p?.firstName); | ||
const getLastName = createGetter((p?: Person) => p?.lastName); | ||
|
||
it('gets the expected value', () => { | ||
expect(getFirstName(alice)).toBe('Alice'); | ||
expect(getFirstName.optional(alice)).toBe('Alice'); | ||
|
||
expect(getLastName(alice)).toBe('Liddell'); | ||
expect(getLastName.optional(alice)).toBe('Liddell'); | ||
}); | ||
|
||
describe('undefined in the getter', () => { | ||
it('handles undefined input', () => { | ||
expect(() => getFirstName(undefined)).toThrow(); | ||
expect(getFirstName.optional(undefined)).toBeUndefined(); | ||
}); | ||
|
||
it('throws for an undefined property', () => { | ||
expect(() => getLastName(employee)).toThrow(); | ||
it('handles undefined property', () => { | ||
expect(() => getLastName(bob)).toThrow(); | ||
expect(getLastName.optional(bob)).toBeUndefined(); | ||
}); | ||
}); | ||
|
||
it('does not throw if a value is falsey but not undefined', () => { | ||
const employee: Employee = { firstName: 'Alice', lastName: '' }; | ||
expect(() => getLastName(employee)).not.toThrow(); | ||
}); | ||
test('successfully returns a falsy value', () => { | ||
expect(() => getLastName(charlie)).not.toThrow(); | ||
}); | ||
}); | ||
|
||
describe('getter.optional()', () => { | ||
it('returns `undefined` when the whole value is undefined', () => { | ||
expect(getLastName.optional()(undefined)).toBeUndefined(); | ||
}); | ||
describe('getter pipes', () => { | ||
const selectAddress = (p?: Person) => p?.address; | ||
const getAddressFromPerson = createGetter(selectAddress); | ||
const selectCity = (a?: Address) => a?.city; | ||
const getCityFromAddress = createGetter(selectCity); | ||
const getStateFromAddress = createGetter((a?: Address) => a?.state); | ||
|
||
it('returns `undefined` for an undefined property', () => { | ||
expect(getLastName.optional()(employee)).toBeUndefined(); | ||
}); | ||
it('pipes the getters together and returns the final result', () => { | ||
let getCityFromPerson; | ||
|
||
expectTypeOf((getCityFromPerson = getAddressFromPerson.pipe(getCityFromAddress))).toEqualTypeOf< | ||
Getter<Person, City> | ||
>(); | ||
|
||
expect(getCityFromPerson(alice)).toBe('San Francisco'); | ||
expect(getCityFromPerson(bob)).toBe('Seattle'); | ||
}); | ||
|
||
describe('getter.pipe()', () => { | ||
it('pipes the getters together and returns the final result', () => { | ||
expect(getAddress.pipe(getCity)(employee)).toBe('San Francisco'); | ||
}); | ||
describe('undefined in the pipe', () => { | ||
let getStateFromPerson; | ||
|
||
expectTypeOf( | ||
(getStateFromPerson = getAddressFromPerson.pipe(getStateFromAddress)), | ||
).toEqualTypeOf<Getter<Person, string>>(); | ||
|
||
it('throws when any value in the property chain is undefined', () => { | ||
expect(() => getAddress.pipe(getCity)(undefined)).toThrow(); | ||
expect(() => getAddress.pipe(getCity)({ firstName: 'Alice' })).toThrow(); | ||
expect(() => getAddress.pipe(getCountry)(employee)).toThrow(); | ||
it('throws on undefined', () => { | ||
expect(() => getStateFromPerson(undefined)).toThrow(); | ||
expect(getStateFromPerson(alice)).toBe('California'); | ||
expect(() => getStateFromPerson(bob)).toThrow(); | ||
}); | ||
|
||
describe('getter.pipe() with .optional())', () => { | ||
const employee: Employee = { | ||
firstName: 'Alice', | ||
address: { | ||
city: '', // `getFirstLetter` will return undefined | ||
state: 'California', | ||
}, | ||
}; | ||
|
||
it('does not throw when the first getter is used with `.optional()` and some value in the chain is undefined', () => { | ||
expect(() => | ||
getAddress.optional().pipe(getCity).pipe(getFirstLetter)(employee), | ||
).not.toThrow(); | ||
}); | ||
|
||
it('does not throw when a later getter is used with `.optional()` and some value in the chain is undefined', () => { | ||
const baseGetter = getAddress.pipe(getCity).pipe(getFirstLetter); | ||
|
||
// Before testing that it _doesn't_ throw with `.optional()`, first make | ||
// sure that it _does_ throw without it, to ensure that this test is | ||
// valid. | ||
expect(() => baseGetter(employee)).toThrow(); | ||
expect(() => baseGetter.optional()(employee)).not.toThrow(); | ||
}); | ||
|
||
it('does throw when used without `.optional()` and some value in the chain is undefined', () => { | ||
expect(() => getAddress.pipe(getCity).pipe(getFirstLetter)(employee)).toThrow(); | ||
}); | ||
it("doesn't throw on undefined when optional", () => { | ||
expect(getStateFromPerson.optional(undefined)).toBeUndefined(); | ||
expect(getStateFromPerson.optional(alice)).toBe('California'); | ||
expect(getStateFromPerson.optional(bob)).toBeUndefined(); | ||
}); | ||
}); | ||
|
||
// Type assertions - these will be run at build time, rather than at test | ||
// time. | ||
assertType<string>(getAddress.pipe(getCity)(employee)); | ||
// @ts-expect-error - Assert that `string` on its own is incorrect for an | ||
// optional getter -- it should be `string | undefined`. | ||
assertType<string>(getAddress.pipe(getCity).optional()(employee)); | ||
assertType<string | undefined>(getAddress.pipe(getCity).optional()(employee)); | ||
describe('longer chains', () => { | ||
const getSecondLetter = createGetter((s?: string) => s?.[1]); | ||
it('applies optional to the chain', () => { | ||
const notOptionalStateLetter = getAddressFromPerson | ||
.pipe(getStateFromAddress) | ||
.pipe(getSecondLetter); | ||
expect(notOptionalStateLetter(alice)).toBe('a'); | ||
expect(() => notOptionalStateLetter(bob)).toThrow(); | ||
expect(() => notOptionalStateLetter(charlie)).toThrow(); | ||
|
||
const notOptionalCityLetter = getAddressFromPerson | ||
.pipe(getCityFromAddress) | ||
.pipe(getSecondLetter); | ||
expect(notOptionalCityLetter(alice)).toBe('a'); | ||
expect(notOptionalCityLetter(bob)).toBe('e'); | ||
expect(() => notOptionalCityLetter(charlie)).toThrow(); | ||
|
||
const midOptionalStateLetter = getAddressFromPerson // address is required | ||
.pipe(getStateFromAddress) // state is optional, turning the whole chain optional | ||
.optional.pipe(getSecondLetter); // letter is required | ||
expect(midOptionalStateLetter(alice)).toBe('a'); | ||
expect(midOptionalStateLetter(bob)).toBeUndefined(); | ||
expect(midOptionalStateLetter(charlie)).toBeUndefined(); | ||
|
||
const midOptionalCityLetter = getAddressFromPerson // address is required | ||
.pipe(getCityFromAddress) // city is optional, turning the whole chain optional | ||
.optional.pipe(getSecondLetter); // letter is required | ||
expect(midOptionalCityLetter(alice)).toBe('a'); | ||
expect(midOptionalCityLetter(bob)).toBe('e'); | ||
expect(midOptionalCityLetter(charlie)).toBeUndefined(); | ||
|
||
const firstOptionalStateLetter = getAddressFromPerson.optional // address is optional, turning the whole chain optional | ||
.pipe(getStateFromAddress) // state is required | ||
.pipe(getSecondLetter); // letter is required | ||
expect(firstOptionalStateLetter(alice)).toBe('a'); | ||
expect(firstOptionalStateLetter(bob)).toBeUndefined(); | ||
expect(firstOptionalStateLetter(charlie)).toBeUndefined(); | ||
|
||
const firstOptionalCityLetter = getAddressFromPerson.optional // address is optional, turning the whole chain optional | ||
.pipe(getCityFromAddress) // city is required | ||
.pipe(getSecondLetter); // letter is required | ||
expect(firstOptionalCityLetter(alice)).toBe('a'); | ||
expect(firstOptionalCityLetter(bob)).toBe('e'); | ||
expect(firstOptionalCityLetter(charlie)).toBeUndefined(); | ||
}); | ||
|
||
it('applies required to the chain', () => { | ||
const allOptionalStateLetter = getAddressFromPerson.optional | ||
.pipe(getStateFromAddress) | ||
.optional.pipe(getSecondLetter).optional; | ||
expect(allOptionalStateLetter.required(alice)).toBe('a'); | ||
expect(() => allOptionalStateLetter.required(bob)).toThrow(); | ||
expect(() => allOptionalStateLetter.required(charlie)).toThrow(); | ||
|
||
const allOptionalCityLetter = getAddressFromPerson.optional | ||
.pipe(getCityFromAddress) | ||
.optional.pipe(getSecondLetter).optional; | ||
expect(allOptionalCityLetter.required(alice)).toBe('a'); | ||
expect(allOptionalCityLetter.required(bob)).toBe('e'); | ||
expect(() => allOptionalCityLetter.required(charlie)).toThrow(); | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.