├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── issue-report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE │ └── pull_request_template.md └── workflows │ ├── npm-publish.yml │ └── main.yml ├── .prettierrc.json ├── src ├── eitherMatchers │ ├── index.ts │ ├── toBeEither.ts │ └── __tests__ │ │ └── toBeEither.test.ts ├── predicates │ ├── isFalsy.ts │ ├── isTruthy.ts │ ├── isError.ts │ ├── hasProperty.ts │ ├── __tests__ │ │ ├── isFalsy.test.ts │ │ ├── isTruthy.test.ts │ │ ├── containsMatch.test.ts │ │ ├── isEither.test.ts │ │ ├── matches.test.ts │ │ ├── containsMatches.test.ts │ │ ├── isError.test.ts │ │ ├── isThese.test.ts │ │ ├── isValidtion.test.ts │ │ ├── contains.test.ts │ │ ├── hasProperty.test.ts │ │ └── equals.test.ts │ ├── isOption.ts │ ├── contains.ts │ ├── isEither.ts │ ├── containsMatches.ts │ ├── matches.ts │ ├── isValidation.ts │ ├── isThese.ts │ ├── isEitherOrThese.ts │ ├── containsMatch.ts │ ├── index.ts │ └── equals.ts ├── decodeMatchers │ └── index.ts ├── theseMatchers │ ├── index.ts │ ├── toBeBoth.ts │ ├── toBeThese.ts │ ├── __tests__ │ │ ├── toBeBoth.test.ts │ │ ├── toBeThese.test.ts │ │ ├── toEqualBoth.test.ts │ │ ├── toStrictEqualBoth.test.ts │ │ └── toSubsetEqualBoth.test.ts │ ├── toEqualBoth.ts │ ├── toStrictEqualBoth.ts │ └── toSubsetEqualBoth.ts ├── either │ ├── print.ts │ ├── applyPredicate.ts │ ├── either.ts │ └── __tests__ │ │ ├── applyPredicate.test.ts │ │ └── print.test.ts ├── optionMatchers │ ├── index.ts │ ├── __tests__ │ │ ├── toBeSome.test.ts │ │ ├── toBeNone.test.ts │ │ ├── toBeOption.test.ts │ │ ├── toEqualSome.test.ts │ │ ├── toStrictEqualSome.test.ts │ │ └── toSubsetEqualSome.test.ts │ ├── toBeOption.ts │ ├── toBeNone.ts │ ├── toBeSome.ts │ ├── toEqualSome.ts │ ├── toStrictEqualSome.ts │ └── toSubsetEqualSome.ts ├── option │ ├── applyPredicate.ts │ ├── __tests__ │ │ ├── applyPredicate.test.ts │ │ └── print.test.ts │ ├── option.ts │ └── print.ts ├── these │ ├── applyPredicate.ts │ ├── these.ts │ ├── __tests__ │ │ └── applyPredicate.test.ts │ └── print.ts ├── eitherOrTheseMatchers │ ├── index.ts │ ├── toBeLeft.ts │ ├── toBeRight.ts │ ├── toEqualRight.ts │ ├── toStrictEqualLeft.ts │ ├── toStrictEqualRight.ts │ ├── toEqualLeft.ts │ ├── toBeLeftErrorMatching.ts │ ├── __tests__ │ │ ├── toBeLeft.test.ts │ │ ├── toBeRight.test.ts │ │ ├── toEqualLeft.test.ts │ │ ├── toEqualRight.test.ts │ │ ├── toBeLeftErrorMatching.test.ts │ │ └── toStrictEqualRight.test.ts │ ├── toSubsetEqualLeft.ts │ └── toSubsetEqualRight.ts ├── eitherOrThese │ ├── eitherOrThese.ts │ ├── applyPredicate.ts │ ├── __tests__ │ │ └── applyPredicate.test.ts │ └── print.ts ├── validation │ └── validation.ts └── index.ts ├── CONTRIBUTING.md ├── .vscode └── settings.json ├── tsconfig.json ├── jest.config.js ├── LICENSE ├── .gitignore ├── .eslintrc.json ├── package.json └── CHANGELOG.md /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true, 6 | "printWidth": 100 7 | } 8 | -------------------------------------------------------------------------------- /src/eitherMatchers/index.ts: -------------------------------------------------------------------------------- 1 | import { toBeEither } from './toBeEither'; 2 | 3 | const matchers = { 4 | toBeEither, 5 | }; 6 | export { matchers }; 7 | -------------------------------------------------------------------------------- /src/predicates/isFalsy.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types 2 | export const isFalsy = (value: any): boolean => (value ? false : true); 3 | -------------------------------------------------------------------------------- /src/predicates/isTruthy.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types 2 | export const isTruthy = (value: any): boolean => (value ? true : false); 3 | -------------------------------------------------------------------------------- /src/decodeMatchers/index.ts: -------------------------------------------------------------------------------- 1 | import { toBeLeftWithErrorsMatching } from './toBeLeftWithErrorsMatching'; 2 | 3 | const matchers = { 4 | toBeLeftWithErrorsMatching, 5 | }; 6 | export { matchers }; 7 | -------------------------------------------------------------------------------- /src/predicates/isError.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Type guard that returns true if the received value is an Error object, 3 | * false otherwise. 4 | */ 5 | export const isError = (received: unknown): received is Error => received instanceof Error; 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to jest-fp-ts 2 | 3 | If you'd like to help make this into a more general community project, or to add more matchers, I'd 4 | love to hear from you! Please open an issue first to let me know how you're planning to contribute. 5 | -------------------------------------------------------------------------------- /src/predicates/hasProperty.ts: -------------------------------------------------------------------------------- 1 | export const hasProperty = 2 | (property: PropertyKey) => 3 | (value: unknown): boolean => { 4 | return value !== null && value !== undefined && {}.hasOwnProperty.call(value, property) 5 | ? true 6 | : false; 7 | }; 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "dist": false 4 | }, 5 | "search.exclude": { 6 | "dist": true, 7 | "package-lock.json": true 8 | }, 9 | "typescript.tsc.autoDetect": "on", 10 | "editor.tabSize": 2, 11 | "jestrunner.jestCommand": "npm run jest --", 12 | "prettier.printWidth": 100 13 | } 14 | -------------------------------------------------------------------------------- /src/predicates/__tests__/isFalsy.test.ts: -------------------------------------------------------------------------------- 1 | import { isFalsy } from '../index'; 2 | 3 | describe('isFalsey', () => { 4 | it('returns true for a falsy value', () => { 5 | expect(isFalsy(null)).toBe(true); 6 | }); 7 | it('returns false for for a truthy value', () => { 8 | expect(isFalsy(-10)).toBe(false); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /src/predicates/__tests__/isTruthy.test.ts: -------------------------------------------------------------------------------- 1 | import { isTruthy } from '../index'; 2 | 3 | describe('isTruthy', () => { 4 | it('returns true for for a truthy value', () => { 5 | expect(isTruthy(-10)).toBe(true); 6 | }); 7 | it('returns false for a falsy value', () => { 8 | expect(isTruthy(null)).toBe(false); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /src/predicates/isOption.ts: -------------------------------------------------------------------------------- 1 | import * as t from 'io-ts'; 2 | import { option } from '../option/option'; 3 | import { Option } from 'fp-ts/lib/Option'; 4 | 5 | /** 6 | * Returns true if the received value is an Option, false otherwise. 7 | */ 8 | export const isOption = (received: unknown): received is Option => 9 | option(t.unknown).is(received); 10 | -------------------------------------------------------------------------------- /src/predicates/contains.ts: -------------------------------------------------------------------------------- 1 | import { equals } from '@jest/expect-utils'; 2 | 3 | /** 4 | * Returns true if the received array contains the expected value 5 | */ 6 | export const contains = 7 | (expectedValue: T) => 8 | (receivedArray: Array | ReadonlyArray): boolean => { 9 | return receivedArray.findIndex((item) => equals(item, expectedValue)) > -1; 10 | }; 11 | -------------------------------------------------------------------------------- /src/predicates/isEither.ts: -------------------------------------------------------------------------------- 1 | import * as t from 'io-ts'; 2 | import { either } from '../either/either'; 3 | import { Either } from 'fp-ts/lib/Either'; 4 | 5 | /** 6 | * Type Guard that returns true if the received value is an Either, false otherwise. 7 | */ 8 | export const isEither = (received: unknown): received is Either => 9 | either(t.unknown, t.unknown).is(received); 10 | -------------------------------------------------------------------------------- /src/predicates/containsMatches.ts: -------------------------------------------------------------------------------- 1 | import { containsMatch } from './containsMatch'; 2 | 3 | export const containsMatches = 4 | (expectedValues: Array) => 5 | (receivedArray: Array | ReadonlyArray): boolean => { 6 | return expectedValues 7 | .map((value) => containsMatch(receivedArray, value)) 8 | .reduce((acc, cur) => acc && cur, true); 9 | }; 10 | -------------------------------------------------------------------------------- /src/theseMatchers/index.ts: -------------------------------------------------------------------------------- 1 | import { toBeThese } from './toBeThese'; 2 | import { toBeBoth } from './toBeBoth'; 3 | import { toEqualBoth } from './toEqualBoth'; 4 | import { toStrictEqualBoth } from './toStrictEqualBoth'; 5 | import { toSubsetEqualBoth } from './toSubsetEqualBoth'; 6 | const matchers = { 7 | toBeThese, 8 | toBeBoth, 9 | toEqualBoth, 10 | toStrictEqualBoth, 11 | toSubsetEqualBoth, 12 | }; 13 | export { matchers }; 14 | -------------------------------------------------------------------------------- /src/either/print.ts: -------------------------------------------------------------------------------- 1 | import { Either, fold } from 'fp-ts/Either'; 2 | import { printReceived } from 'jest-matcher-utils'; 3 | 4 | /** 5 | * Construct a string that shows the received Either value. 6 | */ 7 | export function printReceivedValue(received: Either): string { 8 | return fold( 9 | (left) => `Received Left: ${printReceived(left)}`, 10 | (right) => `Received Right: ${printReceived(right)}`, 11 | )(received); 12 | } 13 | -------------------------------------------------------------------------------- /src/predicates/matches.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns true if the received string matches the expected value meaning 3 | * the received string contains the expected string as a substring or matches 4 | * the supplied regular expression. 5 | */ 6 | export const matches = (received: string, expected: string | RegExp): boolean => { 7 | return typeof expected === 'string' 8 | ? received.includes(expected) 9 | : new RegExp(expected).test(received); 10 | }; 11 | -------------------------------------------------------------------------------- /src/predicates/isValidation.ts: -------------------------------------------------------------------------------- 1 | import * as t from 'io-ts'; 2 | import { validation } from '../validation/validation'; 3 | 4 | /** 5 | * Type Guard that returns true if the received value is an io-ts Validation, 6 | * false otherwise. 7 | * 8 | * A Validiation is the type returned by classic (stable) io-ts decoders 9 | */ 10 | export const isValidation = (received: unknown): received is t.Validation => 11 | validation(t.unknown).is(received); 12 | -------------------------------------------------------------------------------- /src/optionMatchers/index.ts: -------------------------------------------------------------------------------- 1 | import { toBeOption } from './toBeOption'; 2 | import { toBeNone } from './toBeNone'; 3 | import { toBeSome } from './toBeSome'; 4 | import { toEqualSome } from './toEqualSome'; 5 | import { toStrictEqualSome } from './toStrictEqualSome'; 6 | import { toSubsetEqualSome } from './toSubsetEqualSome'; 7 | 8 | const matchers = { 9 | toBeOption, 10 | toBeNone, 11 | toBeSome, 12 | toEqualSome, 13 | toStrictEqualSome, 14 | toSubsetEqualSome, 15 | }; 16 | export { matchers }; 17 | -------------------------------------------------------------------------------- /src/option/applyPredicate.ts: -------------------------------------------------------------------------------- 1 | import { Option, fold } from 'fp-ts/lib/Option'; 2 | import { constFalse } from 'fp-ts/lib/function'; 3 | 4 | /** 5 | * Apply the supplied predicate function, returning false if the received 6 | * value is not a Some or if the receiced value is a Some and the predicate 7 | * function returns false. 8 | */ 9 | export const applyPredicate = 10 | (predicate: (someValue: unknown) => boolean) => 11 | (received: Option): boolean => 12 | fold(constFalse, predicate)(received); 13 | -------------------------------------------------------------------------------- /src/predicates/isThese.ts: -------------------------------------------------------------------------------- 1 | import * as t from 'io-ts'; 2 | import { these } from '../these/these'; 3 | import { These } from 'fp-ts/lib/These'; 4 | 5 | /** 6 | * Type Guard that returns true if the received value is a These, false otherwise. 7 | * 8 | * Note: These is a superset of Either, so this function will also return 9 | * True if the received value is an Either. 10 | */ 11 | export const isThese = (received: unknown): received is These => 12 | these(t.unknown, t.unknown).is(received); 13 | -------------------------------------------------------------------------------- /src/predicates/isEitherOrThese.ts: -------------------------------------------------------------------------------- 1 | import * as t from 'io-ts'; 2 | import { these } from '../these/these'; 3 | import { either } from '../either/either'; 4 | import { EitherOrThese } from '../eitherOrThese/eitherOrThese'; 5 | 6 | /** 7 | * Type guard that returns true if the received value is an Either or a These, false otherwise 8 | */ 9 | export const isEitherOrThese = (received: unknown): received is EitherOrThese => 10 | either(t.unknown, t.unknown).is(received) || these(t.unknown, t.unknown).is(received); 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "lib": ["es2019"], 7 | "outDir": "dist", 8 | "declaration": true, 9 | "sourceMap": true, 10 | "strict": true, 11 | "esModuleInterop": true, 12 | "preserveConstEnums": true, 13 | "removeComments": false, 14 | "noImplicitAny": true, 15 | "noImplicitThis": true, 16 | "noImplicitReturns": true, 17 | "noFallthroughCasesInSwitch": true, 18 | }, 19 | "exclude": ["dist", "node_modules", "**/__tests__/*"] 20 | } 21 | -------------------------------------------------------------------------------- /src/predicates/containsMatch.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns true if the received array contains a match for the expected value 3 | */ 4 | export const containsMatch = ( 5 | receivedArray: Array | ReadonlyArray, 6 | expectedValue: string | RegExp, 7 | ): boolean => { 8 | const matchesItem = 9 | (expected: string | RegExp) => 10 | (received: string): boolean => { 11 | return typeof expected === 'string' 12 | ? received.includes(expected) 13 | : new RegExp(expected).test(received); 14 | }; 15 | return receivedArray.findIndex((item) => matchesItem(expectedValue)(item)) > -1; 16 | }; 17 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | testPathIgnorePatterns: ['/node_modules/'], 5 | coverageDirectory: './dist/coverage', 6 | coveragePathIgnorePatterns: ['/node_modules/'], 7 | snapshotSerializers: [ 8 | '@relmify/jest-serializer-strip-ansi/always', 9 | 'jest-snapshot-serializer-raw/always', 10 | ], 11 | slowTestThreshold: 10, 12 | watchPlugins: [ 13 | ['jest-watch-toggle-config', { setting: 'verbose' }], 14 | ['jest-watch-toggle-config', { setting: 'collectCoverage' }], 15 | ['jest-watch-toggle-config', { setting: 'bail' }], 16 | ], 17 | }; 18 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | ### What 10 | 11 | 12 | ### Why 13 | 14 | 15 | ### Notes 16 | 17 | ### Housekeeping 18 | 19 | - [ ] Unit tests 20 | - [ ] Documentation is up to date 21 | - [ ] No lint warnings 22 | -------------------------------------------------------------------------------- /src/predicates/index.ts: -------------------------------------------------------------------------------- 1 | export { isTruthy } from './isTruthy'; 2 | export { isFalsy } from './isFalsy'; 3 | export { hasProperty } from './hasProperty'; 4 | export { equals, strictEquals, subsetEquals } from './equals'; 5 | export { contains } from './contains'; 6 | export { containsMatch } from './containsMatch'; 7 | export { containsMatches } from './containsMatches'; 8 | export { isEither } from './isEither'; 9 | export { isError } from './isError'; 10 | export { isOption } from './isOption'; 11 | export { isThese } from './isThese'; 12 | export { isEitherOrThese } from './isEitherOrThese'; 13 | export { isValidation } from './isValidation'; 14 | export { matches } from './matches'; 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/issue-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Issue report 3 | about: Something not behaving as expected? Let us know! 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the issue** 11 | A short description of what the issue is. 12 | 13 | **To Reproduce** 14 | Steps, sample code, and any relevant configuration information needed to reproduce the issue. 15 | 16 | ```ts 17 | 18 | ``` 19 | 20 | **Expected behavior** 21 | A short description of what you expected to happen. 22 | 23 | **Suggested fix** 24 | If you've investigated enough to have an idea how to fix this issue, please share! 25 | 26 | **Additional context** 27 | Add any other context about the problem here. 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to npm when a release is created 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages 3 | 4 | name: Publish to npm 5 | 6 | on: 7 | release: 8 | types: [published] 9 | 10 | jobs: 11 | publish-npm: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: actions/setup-node@v3 16 | with: 17 | node-version: '18' 18 | registry-url: https://registry.npmjs.org/ 19 | - run: npm ci 20 | - run: npm publish --access public 21 | env: 22 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 23 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | workflow_dispatch: 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Setup Node 17 | uses: actions/setup-node@v3 18 | with: 19 | node-version: '18' 20 | 21 | - name: Checkout 22 | uses: actions/checkout@v3 23 | 24 | - name: Install 25 | run: npm ci --ignore-scripts 26 | 27 | - name: Lint 28 | run: npm run lint:check 29 | 30 | - name: Prettier 31 | run: npm run prettier:check 32 | 33 | - name: Test 34 | run: npm test 35 | 36 | - name: Build 37 | run: npm run build 38 | -------------------------------------------------------------------------------- /src/these/applyPredicate.ts: -------------------------------------------------------------------------------- 1 | import { These, fold } from 'fp-ts/lib/These'; 2 | import { constFalse } from 'fp-ts/lib/function'; 3 | 4 | /** 5 | * Apply the supplied predicate functions to the Left and Right values of a Both, returning false if 6 | * the received value is not a Both or if the received value is a Both and the predicate functions 7 | * do not return True for both values. 8 | */ 9 | export const applyPredicateBoth = 10 | (predicateLeft: (value: unknown) => boolean, predicateRight: (value: unknown) => boolean) => 11 | (received: These): boolean => 12 | fold( 13 | constFalse, 14 | constFalse, 15 | (leftValue, rightValue) => predicateLeft(leftValue) && predicateRight(rightValue), 16 | )(received); 17 | -------------------------------------------------------------------------------- /src/predicates/__tests__/containsMatch.test.ts: -------------------------------------------------------------------------------- 1 | import { containsMatch } from '../index'; 2 | 3 | const anOunceOfPrevention = ['an ounce of prevention', 'is worth a pound of cure']; 4 | 5 | describe('containsMatch()', () => { 6 | it('returns true if the received array contains a match for the expected value', () => { 7 | expect(containsMatch(anOunceOfPrevention, 'pound')).toBe(true); 8 | }); 9 | it('returns false if the received array does not contain a match for the expected value', () => { 10 | expect(containsMatch(anOunceOfPrevention, 'kilogram')).toBe(false); 11 | }); 12 | it('returns true if the received array contains a match for the expected regular expression', () => { 13 | expect(containsMatch(anOunceOfPrevention, /pound/)).toBe(true); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /src/eitherOrTheseMatchers/index.ts: -------------------------------------------------------------------------------- 1 | import { toBeLeft } from './toBeLeft'; 2 | import { toBeLeftErrorMatching } from './toBeLeftErrorMatching'; 3 | import { toBeRight } from './toBeRight'; 4 | import { toEqualLeft } from './toEqualLeft'; 5 | import { toEqualRight } from './toEqualRight'; 6 | import { toStrictEqualLeft } from './toStrictEqualLeft'; 7 | import { toStrictEqualRight } from './toStrictEqualRight'; 8 | import { toSubsetEqualLeft } from './toSubsetEqualLeft'; 9 | import { toSubsetEqualRight } from './toSubsetEqualRight'; 10 | 11 | const matchers = { 12 | toBeLeft, 13 | toBeLeftErrorMatching, 14 | toBeRight, 15 | toEqualLeft, 16 | toEqualRight, 17 | toStrictEqualLeft, 18 | toStrictEqualRight, 19 | toSubsetEqualLeft, 20 | toSubsetEqualRight, 21 | }; 22 | export { matchers }; 23 | -------------------------------------------------------------------------------- /src/option/__tests__/applyPredicate.test.ts: -------------------------------------------------------------------------------- 1 | import { some, none } from 'fp-ts/lib/Option'; 2 | import { applyPredicate } from '../applyPredicate'; 3 | import { isTruthy } from '../../predicates'; 4 | 5 | describe('applyPredicate', () => { 6 | test('returns true if the received value is a Some and the predicate evaluates to true when applied to the value', () => { 7 | expect(applyPredicate(isTruthy)(some('a string'))).toBe(true); 8 | }); 9 | test('returns false if the received value is a Some and the predicate evaluates to false when applied to the value', () => { 10 | expect(applyPredicate(isTruthy)(some(undefined))).toBe(false); 11 | }); 12 | test('returns false if the received value is a None', () => { 13 | expect(applyPredicate(isTruthy)(none)).toBe(false); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /src/predicates/__tests__/isEither.test.ts: -------------------------------------------------------------------------------- 1 | import { isEither } from '../index'; 2 | import { left, right } from 'fp-ts/lib/Either'; 3 | 4 | describe('isEither', () => { 5 | test('should return true if the value is a left', () => { 6 | expect(isEither(left('left value'))).toBe(true); 7 | }); 8 | test('should return true if the value is a right', () => { 9 | expect(isEither(right('right value'))).toBe(true); 10 | }); 11 | test('should return false if the value is neither a left nor a right', () => { 12 | expect(isEither('not an either')).toBe(false); 13 | }); 14 | test('should return false if the value is null', () => { 15 | expect(isEither(null)).toBe(false); 16 | }); 17 | test('should return false if the value is undefined', () => { 18 | expect(isEither(undefined)).toBe(false); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/eitherOrThese/eitherOrThese.ts: -------------------------------------------------------------------------------- 1 | import { Either, Left, Right } from 'fp-ts/lib/Either'; 2 | import { These, Both } from 'fp-ts/lib/These'; 3 | 4 | export type EitherOrThese = Either | These; 5 | 6 | export const isLeft = (fa: EitherOrThese): fa is Left => fa._tag === 'Left'; 7 | export const isRight = (fa: EitherOrThese): fa is Right => fa._tag === 'Right'; 8 | export const isBoth = (fa: EitherOrThese): fa is Both => fa._tag === 'Both'; 9 | 10 | export const fold = 11 | (onLeft: (e: E) => B, onRight: (a: A) => B, onBoth: (e: E, a: A) => B) => 12 | (fa: EitherOrThese): B => { 13 | switch (fa._tag) { 14 | case 'Left': 15 | return onLeft(fa.left); 16 | case 'Right': 17 | return onRight(fa.right); 18 | case 'Both': 19 | return onBoth(fa.left, fa.right); 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /src/predicates/__tests__/matches.test.ts: -------------------------------------------------------------------------------- 1 | import { matches } from '../index'; 2 | 3 | describe('matches', () => { 4 | test('should return true if received exactly matches expected', () => { 5 | expect(matches('Exact', 'Exact')).toBe(true); 6 | }); 7 | test('should return true if expected is a substring of the received value', () => { 8 | expect(matches('A few words', 'few')).toBe(true); 9 | }); 10 | test('should return true if the received value matches the expected regular expression', () => { 11 | expect(matches('A few words', /FEW/i)).toBe(true); 12 | }); 13 | test('should return false if expected is not a substring of the received value', () => { 14 | expect(matches('A few words', 'FEW')).toBe(false); 15 | }); 16 | test('should return false if the received value does not match the expected regular expression', () => { 17 | expect(matches('A few words', /FEW/)).toBe(false); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /src/either/applyPredicate.ts: -------------------------------------------------------------------------------- 1 | import { Either, fold } from 'fp-ts/lib/Either'; 2 | import { constFalse } from 'fp-ts/lib/function'; 3 | 4 | /** 5 | * Apply the supplied predicate function, returning false if the received 6 | * value is not a Right or if the received value is a Right and the predicate 7 | * function returns false. 8 | */ 9 | export const applyPredicateRight = 10 | (predicate: (rightValue: unknown) => boolean) => 11 | (received: Either): boolean => 12 | fold(constFalse, predicate)(received); 13 | 14 | /** 15 | * Apply the supplied predicate function to a Left, returning false if the received 16 | * value is not a Left or if the received value is a Left and the predicate 17 | * function returns false. 18 | */ 19 | export const applyPredicateLeft = 20 | (predicate: (leftValue: unknown) => boolean) => 21 | (received: Either): boolean => 22 | fold(predicate, constFalse)(received); 23 | -------------------------------------------------------------------------------- /src/option/option.ts: -------------------------------------------------------------------------------- 1 | /** Code in this file was copied from io-ts-types/src/option.ts */ 2 | import * as t from 'io-ts'; 3 | import { Option } from 'fp-ts/lib/Option'; 4 | 5 | const None = t.strict({ 6 | _tag: t.literal('None'), 7 | }); 8 | 9 | const someLiteral = t.literal('Some'); 10 | 11 | /** 12 | * Given a codec representing a type `A`, returns a codec representing `Option` that is able to deserialize 13 | * the JSON representation of an `Option`. 14 | */ 15 | export type OptionC = t.Type< 16 | Option>, 17 | Option>, 18 | unknown 19 | >; 20 | 21 | export function option(codec: C, name = `Option<${codec.name}>`): OptionC { 22 | return t.union( 23 | [ 24 | None, 25 | t.strict( 26 | { 27 | _tag: someLiteral, 28 | value: codec, 29 | }, 30 | `Some<${codec.name}>`, 31 | ), 32 | ], 33 | name, 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /src/optionMatchers/__tests__/toBeSome.test.ts: -------------------------------------------------------------------------------- 1 | import { some, none } from 'fp-ts/lib/Option'; 2 | import { toBeSome } from '../../index'; 3 | 4 | expect.extend({ toBeSome }); 5 | 6 | describe('.toBeSome', () => { 7 | test('should pass if the received object is a Some', () => { 8 | expect(some('Some')).toBeSome(); 9 | }); 10 | test('if called as an asymmetric matcher', () => { 11 | expect(some('Some')).toEqual(expect.toBeSome()); 12 | }); 13 | }); 14 | 15 | describe('.toBeSome should fail', () => { 16 | test('if received is a None', () => { 17 | expect(() => expect(none).toBeSome()).toThrowError(); 18 | }); 19 | }); 20 | 21 | describe('not.toBeSome should pass', () => { 22 | test('if the received object is a None', () => { 23 | expect(none).not.toBeSome(); 24 | }); 25 | }); 26 | 27 | describe('.not.toBeSome should fail', () => { 28 | test('if received is a Some', () => { 29 | expect(() => expect(some('Some')).not.toBeSome()).toThrowError(); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /src/eitherOrThese/applyPredicate.ts: -------------------------------------------------------------------------------- 1 | import { EitherOrThese, fold } from './eitherOrThese'; 2 | import { constFalse } from 'fp-ts/lib/function'; 3 | 4 | /** 5 | * Apply the supplied predicate function to a Right, returning false if the received 6 | * value is not a Right or if the received value is a Right and the predicate 7 | * function returns false. 8 | */ 9 | export const applyPredicateRight = 10 | (predicate: (rightValue: unknown) => boolean) => 11 | (received: EitherOrThese): boolean => 12 | fold(constFalse, predicate, constFalse)(received); 13 | 14 | /** 15 | * Apply the supplied predicate function to a Left, returning false if the received 16 | * value is not a Left or if the received value is a Left and the predicate 17 | * function returns false. 18 | */ 19 | export const applyPredicateLeft = 20 | (predicate: (leftValue: unknown) => boolean) => 21 | (received: EitherOrThese): boolean => 22 | fold(predicate, constFalse, constFalse)(received); 23 | -------------------------------------------------------------------------------- /src/option/print.ts: -------------------------------------------------------------------------------- 1 | import { Option, fold } from 'fp-ts/lib/Option'; 2 | import { printExpected, printReceived, printDiffOrStringify } from 'jest-matcher-utils'; 3 | 4 | /** 5 | * Construct a string that either indicates a None was received or prints the value of the Some was received 6 | */ 7 | export function printReceivedOption(received: Option, addPadding = false): string { 8 | const padding = addPadding ? ' ' : ''; 9 | return fold( 10 | () => `Received None`, 11 | (some) => `Received Some: ` + padding + `${printReceived(some)}`, 12 | )(received); 13 | } 14 | 15 | /** 16 | * Construct a string that shows the difference between a received and expected Some value 17 | */ 18 | export function diffReceivedSome(received: Option, expected: unknown): string { 19 | return fold( 20 | () => `Expected Some: ${printExpected(expected)}\n` + printReceivedOption(received), 21 | (some) => printDiffOrStringify(expected, some, 'Expected Some', 'Received Some', true), 22 | )(received); 23 | } 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 relmify 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/predicates/__tests__/containsMatches.test.ts: -------------------------------------------------------------------------------- 1 | import { containsMatches } from '../index'; 2 | 3 | const anOuncOfPrevention = ['an', 'ounce', 'of', 'prevention']; 4 | const ouncePrevention = ['ounce', 'prevention']; 5 | const isWorthAPoundOfCure = ['is', 'worth', 'a', 'pound', 'of', 'cure']; 6 | const preventionIsWorth = ['prevention', 'is', 'worth']; 7 | 8 | describe('containsMatches()', () => { 9 | it('returns true if the received array contains matches for all of the expected values', () => { 10 | expect(containsMatches(ouncePrevention)(anOuncOfPrevention)).toBe(true); 11 | }); 12 | it('returns false if the received array does not contain matches for any of the expected values', () => { 13 | expect(containsMatches(ouncePrevention)(isWorthAPoundOfCure)).toBe(false); 14 | }); 15 | it('returns false if the received array does not contain matches for all of the expected values', () => { 16 | expect(containsMatches(preventionIsWorth)(isWorthAPoundOfCure)).toBe(false); 17 | }); 18 | it('returns false if the received array is empty', () => { 19 | expect(containsMatches(anOuncOfPrevention)([])).toBe(false); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/either/either.ts: -------------------------------------------------------------------------------- 1 | /** Code in this file is based on io-ts-types/src/either.ts */ 2 | import * as t from 'io-ts'; 3 | import { Either } from 'fp-ts/lib/Either'; 4 | 5 | const leftLiteral = t.literal('Left'); 6 | const rightLiteral = t.literal('Right'); 7 | 8 | type EitherT = t.Type< 9 | Either, t.TypeOf>, 10 | Either, t.OutputOf>, 11 | unknown 12 | >; 13 | 14 | /** 15 | * Given a codec representing a type `L` and a codec representing a type `R`, 16 | * returns a codec representing `Either` that is able to deserialize 17 | * the JSON representation of an `Either`. 18 | */ 19 | export const either = ( 20 | leftCodec: L, 21 | rightCodec: R, 22 | name = `Either<${leftCodec.name}, ${rightCodec.name}>`, 23 | ): EitherT => { 24 | return t.union( 25 | [ 26 | t.strict( 27 | { 28 | _tag: leftLiteral, 29 | left: leftCodec, 30 | }, 31 | `Left<${leftCodec.name}>`, 32 | ), 33 | t.strict( 34 | { 35 | _tag: rightLiteral, 36 | right: rightCodec, 37 | }, 38 | `Right<${rightCodec.name}>`, 39 | ), 40 | ], 41 | name, 42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /src/optionMatchers/toBeOption.ts: -------------------------------------------------------------------------------- 1 | import { matcherHint, printReceived } from 'jest-matcher-utils'; 2 | import { isOption } from '../predicates'; 3 | import { printReceivedOption } from '../option/print'; 4 | import { Option } from 'fp-ts/lib/Option'; 5 | 6 | const passMessage = (received: Option) => () => 7 | matcherHint('.not.toBeOption', 'received', '') + '\n\n' + `${printReceivedOption(received)}`; 8 | 9 | const failMessage = (received: unknown) => () => 10 | matcherHint('.toBeOption', 'received', '') + '\n\n' + `Received: ${printReceived(received)}`; 11 | 12 | declare global { 13 | namespace jest { 14 | interface Matchers { 15 | /** 16 | * Used to check if a value is a Option (either a Some or a None). 17 | */ 18 | readonly toBeOption: () => R; 19 | } 20 | interface Expect { 21 | /** 22 | * Used to check if a value is a Option (either a Some or a None). 23 | */ 24 | readonly toBeOption: () => any; 25 | } 26 | } 27 | } 28 | 29 | /** 30 | * Check that the received value is an Option 31 | */ 32 | export const toBeOption = (received: unknown): any => { 33 | const pass = isOption(received); 34 | return { 35 | pass, 36 | message: pass ? passMessage(received) : failMessage(received), 37 | }; 38 | }; 39 | -------------------------------------------------------------------------------- /src/predicates/__tests__/isError.test.ts: -------------------------------------------------------------------------------- 1 | import { isError } from '../index'; 2 | 3 | describe('isError', () => { 4 | test('should return true for a standard error object', () => { 5 | expect(isError(new Error('Standard error object'))).toBe(true); 6 | }); 7 | test('should return true for a TypeError', () => { 8 | expect(isError(new TypeError('a Type Error'))).toBe(true); 9 | }); 10 | test('should return true for a custom class that extends error', () => { 11 | class CustomError extends Error { 12 | constructor(message: string) { 13 | super(message); 14 | Object.defineProperty(this, 'name', { value: 'CustomError' }); 15 | } 16 | } 17 | expect(isError(new CustomError('a Custom Error'))).toBe(true); 18 | }); 19 | test('should return false if the value is not an object', () => { 20 | expect(isError('not an Error')).toBe(false); 21 | }); 22 | test('should return false if the value is a plain object with a name and message', () => { 23 | expect(isError({ name: 'Error', message: 'Message' })).toBe(false); 24 | }); 25 | test('should return false if the value is null', () => { 26 | expect(isError(null)).toBe(false); 27 | }); 28 | test('should return false if the value is undefined', () => { 29 | expect(isError(undefined)).toBe(false); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /src/predicates/__tests__/isThese.test.ts: -------------------------------------------------------------------------------- 1 | import { isThese } from '../index'; 2 | import { left, right, both } from 'fp-ts/lib/These'; 3 | import { left as leftEither, right as rightEither } from 'fp-ts/lib/Either'; 4 | 5 | describe('isThese', () => { 6 | test('should return true if the value is a left (These)', () => { 7 | expect(isThese(left('left value'))).toBe(true); 8 | }); 9 | test('should return true if the value is a left (Either)', () => { 10 | expect(isThese(leftEither('left value'))).toBe(true); 11 | }); 12 | test('should return true if the value is a right (These)', () => { 13 | expect(isThese(right('right value'))).toBe(true); 14 | }); 15 | test('should return true if the value is a right (Either)', () => { 16 | expect(isThese(rightEither('right value'))).toBe(true); 17 | }); 18 | test('should return true if the value is a both', () => { 19 | expect(isThese(both('left value', 'right value'))).toBe(true); 20 | }); 21 | test('should return false if the value is not a left, right, or both', () => { 22 | expect(isThese('not a These')).toBe(false); 23 | }); 24 | test('should return false if the value is null', () => { 25 | expect(isThese(null)).toBe(false); 26 | }); 27 | test('should return false if the value is undefined', () => { 28 | expect(isThese(undefined)).toBe(false); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /src/predicates/__tests__/isValidtion.test.ts: -------------------------------------------------------------------------------- 1 | import { isValidation } from '../index'; 2 | import { left, right } from 'fp-ts/lib/Either'; 3 | import * as t from 'io-ts'; 4 | 5 | const person = t.type({ 6 | name: t.string, 7 | id: t.number, 8 | }); 9 | 10 | describe('isValidation', () => { 11 | test('should return true for the return value of a successful decode', () => { 12 | expect(isValidation(person.decode({ name: 'Nikolai Tesla', id: 1234 }))).toBe(true); 13 | }); 14 | test('should return true for the return vale of a failed decode', () => { 15 | expect(isValidation(person.decode({ name: 'Nikolai Tesla', id: 'string not number' }))).toBe( 16 | true, 17 | ); 18 | }); 19 | test('should return true if the value is a right', () => { 20 | expect(isValidation(right('right value'))).toBe(true); 21 | }); 22 | test('should return false if the value is not an Either', () => { 23 | expect(isValidation('not a Validation')).toBe(false); 24 | }); 25 | test('should return false if the value is a left that does not contain decode Errors', () => { 26 | expect(isValidation(left('left value'))).toBe(false); 27 | }); 28 | test('should return false if the value is null', () => { 29 | expect(isValidation(null)).toBe(false); 30 | }); 31 | test('should return false if the value is undefined', () => { 32 | expect(isValidation(undefined)).toBe(false); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /src/these/these.ts: -------------------------------------------------------------------------------- 1 | import * as t from 'io-ts'; 2 | import { These } from 'fp-ts/lib/These'; 3 | 4 | const leftLiteral = t.literal('Left'); 5 | const rightLiteral = t.literal('Right'); 6 | const bothLiteral = t.literal('Both'); 7 | 8 | type TheseT = t.Type< 9 | These, t.TypeOf>, 10 | These, t.OutputOf>, 11 | unknown 12 | >; 13 | 14 | /** 15 | * Given a codec representing a type `L` and a codec representing a type `R`, 16 | * returns a codec representing `These` that is able to deserialize 17 | * the JSON representation of an `These`. 18 | */ 19 | export const these = ( 20 | leftCodec: L, 21 | rightCodec: R, 22 | name = `These<${leftCodec.name}, ${rightCodec.name}>`, 23 | ): TheseT => { 24 | return t.union( 25 | [ 26 | t.strict( 27 | { 28 | _tag: leftLiteral, 29 | left: leftCodec, 30 | }, 31 | `Left<${leftCodec.name}>`, 32 | ), 33 | t.strict( 34 | { 35 | _tag: rightLiteral, 36 | right: rightCodec, 37 | }, 38 | `Right<${rightCodec.name}>`, 39 | ), 40 | t.strict( 41 | { 42 | _tag: bothLiteral, 43 | left: leftCodec, 44 | right: rightCodec, 45 | }, 46 | `Both<${leftCodec.name}, ${rightCodec.name}>`, 47 | ), 48 | ], 49 | name, 50 | ); 51 | }; 52 | -------------------------------------------------------------------------------- /src/these/__tests__/applyPredicate.test.ts: -------------------------------------------------------------------------------- 1 | import { left, right, both } from 'fp-ts/lib/These'; 2 | import { applyPredicateBoth } from '../applyPredicate'; 3 | import { isTruthy } from '../../predicates'; 4 | 5 | describe('applyPredicateBoth', () => { 6 | test('returns true if the received value is a both and predicates evaluate to true when applied to both values', () => { 7 | expect(applyPredicateBoth(isTruthy, isTruthy)(both(402, 'a string'))).toBe(true); 8 | }); 9 | test('returns false if the received value is a both and predicates evaluate to false when applied both values', () => { 10 | expect(applyPredicateBoth(isTruthy, isTruthy)(both(undefined, undefined))).toBe(false); 11 | }); 12 | test('returns false if the received value is a both but predicate evaluate to true for only the left value', () => { 13 | expect(applyPredicateBoth(isTruthy, isTruthy)(both(true, false))).toBe(false); 14 | }); 15 | test('returns false if the received value is a both but predicates evaluate to true for only the right value', () => { 16 | expect(applyPredicateBoth(isTruthy, isTruthy)(both(false, true))).toBe(false); 17 | }); 18 | test('returns false if the received value is a left', () => { 19 | expect(applyPredicateBoth(isTruthy, isTruthy)(left(true))).toBe(false); 20 | }); 21 | test('returns false if the received value is a right', () => { 22 | expect(applyPredicateBoth(isTruthy, isTruthy)(right(true))).toBe(false); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/optionMatchers/toBeNone.ts: -------------------------------------------------------------------------------- 1 | import { matcherHint, printReceived } from 'jest-matcher-utils'; 2 | import { Option, isNone } from 'fp-ts/lib/Option'; 3 | import { isOption } from '../predicates'; 4 | import { printReceivedOption } from '../option/print'; 5 | 6 | const passMessage = (received: Option) => () => 7 | matcherHint('.not.toBeNone', 'received', '') + '\n\n' + `${printReceivedOption(received)}`; 8 | 9 | const failMessage = (received: unknown) => () => { 10 | return isOption(received) 11 | ? matcherHint('.toBeNone', 'received', '') + '\n\n' + `${printReceivedOption(received)}` 12 | : matcherHint('.toBeNone', 'received', '') + 13 | '\n\n' + 14 | 'Received value is not an Option.\n' + 15 | `Received: ${printReceived(received)}`; 16 | }; 17 | 18 | declare global { 19 | namespace jest { 20 | interface Matchers { 21 | /** 22 | * Used to check if a value is a None. 23 | */ 24 | readonly toBeNone: () => R; 25 | } 26 | interface Expect { 27 | /** 28 | * Used to check if a value is a None. 29 | */ 30 | readonly toBeNone: () => any; 31 | } 32 | } 33 | } 34 | 35 | /** 36 | * Check that the received value is a None 37 | */ 38 | export const toBeNone = (received: unknown): any => { 39 | const pass = isOption(received) && isNone(received); 40 | return { 41 | pass, 42 | message: pass ? passMessage(received) : failMessage(received), 43 | }; 44 | }; 45 | -------------------------------------------------------------------------------- /src/theseMatchers/toBeBoth.ts: -------------------------------------------------------------------------------- 1 | import { matcherHint, printReceived } from 'jest-matcher-utils'; 2 | import { isBoth, These } from 'fp-ts/lib/These'; 3 | import { isThese } from '../predicates'; 4 | import { printReceivedValue } from '../eitherOrThese/print'; 5 | 6 | const passMessage = (received: These) => () => 7 | matcherHint('.not.toBeBoth', 'received', '') + '\n\n' + `${printReceivedValue(received)}`; 8 | 9 | const failMessage = (received: unknown) => () => { 10 | return isThese(received) 11 | ? matcherHint('.toBeBoth', 'received', '') + '\n\n' + `${printReceivedValue(received)}` 12 | : matcherHint('.toBeBoth', 'received', '') + 13 | '\n\n' + 14 | 'Received value is not a These.\n' + 15 | `Received: ${printReceived(received)}`; 16 | }; 17 | 18 | declare global { 19 | namespace jest { 20 | interface Matchers { 21 | /** 22 | * Used to check if a value is a Both. 23 | */ 24 | readonly toBeBoth: () => R; 25 | } 26 | interface Expect { 27 | /** 28 | * Used to check if a value is a Both. 29 | */ 30 | readonly toBeBoth: () => any; 31 | } 32 | } 33 | } 34 | 35 | /** 36 | * Check that the received value is a Both 37 | */ 38 | export const toBeBoth = (received: unknown): any => { 39 | const pass = isThese(received) && isBoth(received); 40 | return { 41 | pass, 42 | message: pass ? passMessage(received) : failMessage(received), 43 | }; 44 | }; 45 | -------------------------------------------------------------------------------- /src/either/__tests__/applyPredicate.test.ts: -------------------------------------------------------------------------------- 1 | import { left, right } from 'fp-ts/lib/Either'; 2 | import { applyPredicateRight, applyPredicateLeft } from '../applyPredicate'; 3 | import { isTruthy } from '../../predicates'; 4 | 5 | describe('applyPredicate', () => { 6 | test('returns true if the received value is a right and the predicate evaluates to true when applied to the value', () => { 7 | expect(applyPredicateRight(isTruthy)(right('a string'))).toBe(true); 8 | }); 9 | test('returns false if the received value is a right and the predicate evaluates to false when applied to the value', () => { 10 | expect(applyPredicateRight(isTruthy)(right(undefined))).toBe(false); 11 | }); 12 | test('returns false if the received value is a left', () => { 13 | expect(applyPredicateRight(isTruthy)(left(true))).toBe(false); 14 | }); 15 | }); 16 | 17 | describe('applyPredicateLeft', () => { 18 | test('returns true if the received value is a left and the predicate evaluates to true when applied to the value', () => { 19 | expect(applyPredicateLeft(isTruthy)(left('a string'))).toBe(true); 20 | }); 21 | test('returns false if the received value is a left and the predicate evaluates to false when applied to the value', () => { 22 | expect(applyPredicateLeft(isTruthy)(left(undefined))).toBe(false); 23 | }); 24 | test('returns false if the received value is a right', () => { 25 | expect(applyPredicateLeft(isTruthy)(right(true))).toBe(false); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/optionMatchers/toBeSome.ts: -------------------------------------------------------------------------------- 1 | import { matcherHint, printReceived } from 'jest-matcher-utils'; 2 | import { Option, isSome } from 'fp-ts/lib/Option'; 3 | import { isOption } from '../predicates'; 4 | import { printReceivedOption } from '../option/print'; 5 | 6 | const passMessage = (received: Option) => () => 7 | matcherHint('.not.toBeSome', 'received', '') + '\n\n' + `${printReceivedOption(received)}`; 8 | 9 | const failMessage = (received: unknown) => () => { 10 | return isOption(received) 11 | ? matcherHint('.toBeSome', 'received', '') + '\n\n' + `${printReceivedOption(received)}` 12 | : matcherHint('.toBeSome', 'received', '') + 13 | '\n\n' + 14 | 'Received value is not an Option.\n' + 15 | `Received: ${printReceived(received)}`; 16 | }; 17 | 18 | declare global { 19 | namespace jest { 20 | interface Matchers { 21 | /** 22 | * Used to check if a value is a Some. 23 | */ 24 | readonly toBeSome: () => R; 25 | } 26 | interface Expect { 27 | /** 28 | * Used to check if a value is a Some. 29 | */ 30 | readonly toBeSome: () => any; 31 | } 32 | } 33 | } 34 | 35 | /** 36 | * Check that the received value is a Some 37 | */ 38 | export const toBeSome = (received: unknown): any => { 39 | const pass = isOption(received) && isSome(received); 40 | 41 | return { 42 | pass: pass, 43 | message: pass ? passMessage(received) : failMessage(received), 44 | }; 45 | }; 46 | -------------------------------------------------------------------------------- /src/eitherOrThese/__tests__/applyPredicate.test.ts: -------------------------------------------------------------------------------- 1 | import { left, right } from 'fp-ts/lib/Either'; 2 | import { applyPredicateRight, applyPredicateLeft } from '../applyPredicate'; 3 | import { isTruthy } from '../../predicates'; 4 | 5 | describe('applyPredicateRight', () => { 6 | test('returns true if the received value is a right and the predicate evaluates to true when applied to the value', () => { 7 | expect(applyPredicateRight(isTruthy)(right('a string'))).toBe(true); 8 | }); 9 | test('returns false if the received value is a right and the predicate evaluates to false when applied to the value', () => { 10 | expect(applyPredicateRight(isTruthy)(right(undefined))).toBe(false); 11 | }); 12 | test('returns false if the received value is a left', () => { 13 | expect(applyPredicateRight(isTruthy)(left(true))).toBe(false); 14 | }); 15 | }); 16 | 17 | describe('applyPredicateLeft', () => { 18 | test('returns true if the received value is a left and the predicate evaluates to true when applied to the value', () => { 19 | expect(applyPredicateLeft(isTruthy)(left('a string'))).toBe(true); 20 | }); 21 | test('returns false if the received value is a left and the predicate evaluates to false when applied to the value', () => { 22 | expect(applyPredicateLeft(isTruthy)(left(undefined))).toBe(false); 23 | }); 24 | test('returns false if the received value is a right', () => { 25 | expect(applyPredicateLeft(isTruthy)(right(true))).toBe(false); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/optionMatchers/__tests__/toBeNone.test.ts: -------------------------------------------------------------------------------- 1 | import { some, none } from 'fp-ts/lib/Option'; 2 | import { toBeNone } from '../../index'; 3 | 4 | expect.extend({ toBeNone }); 5 | 6 | describe('.toBeNone should pass', () => { 7 | test('if received is a None', () => { 8 | expect(none).toBeNone(); 9 | }); 10 | test('if called as an asymmetric matcher', () => { 11 | expect(none).toEqual(expect.toBeNone()); 12 | }); 13 | }); 14 | 15 | describe('.toBeNone should fail', () => { 16 | test('if received is a Some', () => { 17 | expect(() => expect(some(1)).toBeNone()).toThrowErrorMatchingInlineSnapshot(` 18 | expect(received).toBeNone() 19 | 20 | Received Some: 1 21 | `); 22 | }); 23 | test('if received is not an Option', () => { 24 | expect(() => expect(null).toBeNone()).toThrowErrorMatchingInlineSnapshot(` 25 | expect(received).toBeNone() 26 | 27 | Received value is not an Option. 28 | Received: null 29 | `); 30 | }); 31 | }); 32 | 33 | describe('.not.toBeNone should pass', () => { 34 | test('if received is a Some', () => { 35 | expect(some('Some')).not.toBeNone(); 36 | }); 37 | test('if received is not an Option', () => { 38 | expect(null).not.toBeNone(); 39 | }); 40 | }); 41 | 42 | describe('.not.toBeNone should fail', () => { 43 | test('if received is a None', () => { 44 | expect(() => expect(none).not.toBeNone()).toThrowErrorMatchingInlineSnapshot(` 45 | expect(received).not.toBeNone() 46 | 47 | Received None 48 | `); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /src/either/__tests__/print.test.ts: -------------------------------------------------------------------------------- 1 | import { left, right } from 'fp-ts/lib/Either'; 2 | import { printReceivedValue } from '../print'; 3 | 4 | describe('printReceivedValue includes the received value', () => { 5 | test('if a Left with a string value is received', () => { 6 | expect(printReceivedValue(left('Left value'))).toMatchInlineSnapshot( 7 | `Received Left: "Left value"`, 8 | ); 9 | }); 10 | test('if a Left with an object value is received', () => { 11 | expect(printReceivedValue(left({ id: 1 }))).toMatchInlineSnapshot(`Received Left: {"id": 1}`); 12 | }); 13 | test('if a Left with an undefined value is received', () => { 14 | expect(printReceivedValue(left(undefined))).toMatchInlineSnapshot(`Received Left: undefined`); 15 | }); 16 | test('if a Right with an Error value is received', () => { 17 | const errorRight = right(new Error('A Right with an Error value')); 18 | expect(printReceivedValue(errorRight)).toMatchInlineSnapshot( 19 | `Received Right: [Error: A Right with an Error value]`, 20 | ); 21 | }); 22 | test('if a Right with an object value is received', () => { 23 | expect(printReceivedValue(right({ id: 1 }))).toMatchInlineSnapshot(`Received Right: {"id": 1}`); 24 | }); 25 | test('if a Right with an undefined value is received', () => { 26 | expect(printReceivedValue(right(undefined))).toMatchInlineSnapshot(`Received Right: undefined`); 27 | }); 28 | test('if a Right with a number value is received', () => { 29 | expect(printReceivedValue(right(42))).toMatchInlineSnapshot(`Received Right: 42`); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /src/eitherOrTheseMatchers/toBeLeft.ts: -------------------------------------------------------------------------------- 1 | import { matcherHint, printReceived } from 'jest-matcher-utils'; 2 | import { isEitherOrThese } from '../predicates'; 3 | import { isLeft } from '../eitherOrThese/eitherOrThese'; 4 | import { EitherOrThese } from '../eitherOrThese/eitherOrThese'; 5 | import { printReceivedValue } from '../eitherOrThese/print'; 6 | 7 | const passMessage = (received: EitherOrThese) => () => 8 | matcherHint('.not.toBeLeft', 'received', '') + '\n\n' + `${printReceivedValue(received)}`; 9 | 10 | const failMessage = (received: unknown) => () => { 11 | return isEitherOrThese(received) 12 | ? matcherHint('.toBeLeft', 'received', '') + '\n\n' + `${printReceivedValue(received)}` 13 | : matcherHint('.toBeLeft', 'received', '') + 14 | '\n\n' + 15 | 'Received value is not an Either or These.\n' + 16 | `Received: ${printReceived(received)}`; 17 | }; 18 | 19 | declare global { 20 | namespace jest { 21 | interface Matchers { 22 | /** 23 | * Used to check if a value is a Left. 24 | */ 25 | readonly toBeLeft: () => R; 26 | } 27 | interface Expect { 28 | /** 29 | * Used to check if a value is a Left. 30 | */ 31 | readonly toBeLeft: () => any; 32 | } 33 | } 34 | } 35 | 36 | /** 37 | * Check that the received value is a Left 38 | */ 39 | export const toBeLeft = (received: unknown): any => { 40 | const pass = isEitherOrThese(received) && isLeft(received); 41 | return { 42 | pass, 43 | message: pass ? passMessage(received) : failMessage(received), 44 | }; 45 | }; 46 | -------------------------------------------------------------------------------- /src/eitherOrTheseMatchers/toBeRight.ts: -------------------------------------------------------------------------------- 1 | import { matcherHint, printReceived } from 'jest-matcher-utils'; 2 | import { isRight } from '../eitherOrThese/eitherOrThese'; 3 | import { isEitherOrThese } from '../predicates'; 4 | import { printReceivedValue } from '../eitherOrThese/print'; 5 | import { EitherOrThese } from '../eitherOrThese/eitherOrThese'; 6 | 7 | const passMessage = (received: EitherOrThese) => () => 8 | matcherHint('.not.toBeRight', 'received', '') + '\n\n' + `${printReceivedValue(received)}`; 9 | 10 | const failMessage = (received: unknown) => () => { 11 | return isEitherOrThese(received) 12 | ? matcherHint('.toBeRight', 'received', '') + '\n\n' + `${printReceivedValue(received)}` 13 | : matcherHint('.toBeRight', 'received', '') + 14 | '\n\n' + 15 | 'Received value is not an Either or These.\n' + 16 | `Received: ${printReceived(received)}`; 17 | }; 18 | 19 | declare global { 20 | namespace jest { 21 | interface Matchers { 22 | /** 23 | * Used to check if a value is a Right. 24 | */ 25 | readonly toBeRight: () => R; 26 | } 27 | interface Expect { 28 | /** 29 | * Used to check if a value is a Right. 30 | */ 31 | readonly toBeRight: () => any; 32 | } 33 | } 34 | } 35 | 36 | /** 37 | * Checked that the received value is a Right 38 | */ 39 | export const toBeRight = (received: unknown): any => { 40 | const pass = isEitherOrThese(received) && isRight(received); 41 | 42 | return { 43 | pass: pass, 44 | message: pass ? passMessage(received) : failMessage(received), 45 | }; 46 | }; 47 | -------------------------------------------------------------------------------- /src/predicates/__tests__/contains.test.ts: -------------------------------------------------------------------------------- 1 | import { contains } from '../index'; 2 | 3 | describe('contains() returns true with', () => { 4 | test.each([ 5 | [[1, 2, 3], 3, 'an array of numbers that contains the expected number'], 6 | [[3, 3, 3], 3, 'an array of numbers that contains multiple copies of the expected number'], 7 | [['jest', 'is', 'fun'], 'jest', 'an array of strings that contains the expected string'], 8 | [[{ id: 1 }, { id: 2 }], { id: 2 }, 'an array of objects contains the expected object'], 9 | [ 10 | [ 11 | [1, 2], 12 | [3, 4], 13 | ], 14 | [3, 4], 15 | 'an array of arrays that contains the expected array', 16 | ], 17 | ])('received: %p, expected: %p -- %s', (received, expected, description) => { 18 | expect(contains(expected)(received)).toBe(true); 19 | }); 20 | }); 21 | 22 | describe('contains() returns false with', () => { 23 | test.each([ 24 | [[1, 2, 3], 5, 'an array of numbers that does not contain the expected number'], 25 | [ 26 | ['jest', 'is', 'fun'], 27 | 'for testing', 28 | 'an array of strings that does not contain the expected string', 29 | ], 30 | [ 31 | [{ id: 1 }, { id: 2 }], 32 | { id: 22 }, 33 | 'an array of objects does not contain the expected object', 34 | ], 35 | [ 36 | [ 37 | [1, 2], 38 | [3, 4], 39 | ], 40 | [5, 6], 41 | 'an array of arrays that does not contain the expected array', 42 | ], 43 | ])('received: %p, expected: %p -- %s', (received, expected, description) => { 44 | expect(contains(expected)(received)).toBe(false); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /src/predicates/__tests__/hasProperty.test.ts: -------------------------------------------------------------------------------- 1 | import { hasProperty } from '../index'; 2 | 3 | describe('hasProperty', () => { 4 | test('should return false if value is null', () => { 5 | expect(hasProperty('prop')(null)).toEqual(false); 6 | }); 7 | test('should return false if value is undefined', () => { 8 | expect(hasProperty('prop')(undefined)).toEqual(false); 9 | }); 10 | test('should return true if the property exists', () => { 11 | const obj = { prop: 'prop' }; 12 | expect(hasProperty('prop')(obj)).toEqual(true); 13 | }); 14 | test('should handle undefined property values', () => { 15 | const obj = { prop: undefined }; 16 | expect(hasProperty('prop')(obj)).toEqual(true); 17 | }); 18 | test('should handle null property values', () => { 19 | const obj = { prop: null }; 20 | expect(hasProperty('prop')(obj)).toEqual(true); 21 | }); 22 | test('should handle string values', () => { 23 | const str = 'a string'; 24 | expect(hasProperty('length')(str)).toEqual(true); 25 | }); 26 | test('should handle array received values with an index number string as the property', () => { 27 | const arr = ['an', 'array']; 28 | expect(hasProperty('1')(arr)).toEqual(true); 29 | }); 30 | test('should return false if an out of range array index is provided', () => { 31 | const arr = ['an', 'array']; 32 | expect(hasProperty('2')(arr)).toEqual(false); 33 | }); 34 | test('should handle inherited property values', () => { 35 | class StringWithPrefix extends String { 36 | constructor(str: string, prefix = 'why ') { 37 | super(prefix + str); 38 | } 39 | } 40 | const str = new StringWithPrefix('hello'); 41 | expect(hasProperty('length')(str)).toEqual(true); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /src/theseMatchers/toBeThese.ts: -------------------------------------------------------------------------------- 1 | import { matcherHint, printReceived } from 'jest-matcher-utils'; 2 | import { isThese } from '../predicates'; 3 | import { printReceivedValue } from '../eitherOrThese/print'; 4 | import { These } from 'fp-ts/lib/These'; 5 | 6 | const passMessage = (received: These) => () => 7 | matcherHint('.not.toBeThese', 'received', '') + '\n\n' + `${printReceivedValue(received)}`; 8 | 9 | const failMessage = (received: unknown) => () => 10 | matcherHint('.toBeThese', 'received', '') + '\n\n' + `Received: ${printReceived(received)}`; 11 | 12 | declare global { 13 | namespace jest { 14 | interface Matchers { 15 | /** 16 | * Used to check if a value is consistent with a These. In other words, this 17 | * matcher confirms that the value is a Left, a Right, or a Both. 18 | * 19 | * Note that a Left or a Right value is also consistent with an Either and would also pass a 20 | * `.toBeEither()` test. 21 | */ 22 | readonly toBeThese: () => R; 23 | } 24 | interface Expect { 25 | /** 26 | * Used to check if a value is consistent with a These. In other words, this 27 | * matcher confirms that the value is a Left, a Right, or a Both. 28 | * 29 | * Note that a Left or a Right value is also consistent with an Either and would also pass a 30 | * `.toBeEither()` test. 31 | */ 32 | readonly toBeThese: () => any; 33 | } 34 | } 35 | } 36 | 37 | /** 38 | * Matches if the received value is a These 39 | */ 40 | export const toBeThese = (received: unknown): any => { 41 | const pass = isThese(received); 42 | return { 43 | pass, 44 | message: pass ? passMessage(received) : failMessage(received), 45 | }; 46 | }; 47 | -------------------------------------------------------------------------------- /src/optionMatchers/__tests__/toBeOption.test.ts: -------------------------------------------------------------------------------- 1 | import { some, none } from 'fp-ts/lib/Option'; 2 | import { toBeOption } from '../../index'; 3 | 4 | expect.extend({ toBeOption }); 5 | 6 | describe('.toBeOption should pass', () => { 7 | test('if received is a None', () => { 8 | expect(none).toBeOption(); 9 | }); 10 | test('if received is a Some', () => { 11 | expect(some('Some')).toBeOption(); 12 | }); 13 | test('if called as an asymmetric matcher', () => { 14 | expect(some('Some')).toEqual(expect.toBeOption()); 15 | }); 16 | }); 17 | 18 | describe('.toBeOption should fail', () => { 19 | test('if received is neither a Some nor a None', () => { 20 | expect(() => expect(undefined).toBeOption()).toThrowErrorMatchingInlineSnapshot(` 21 | expect(received).toBeOption() 22 | 23 | Received: undefined 24 | `); 25 | }); 26 | }); 27 | 28 | describe('.not.toBeOption should pass', () => { 29 | test('if received is null', () => { 30 | expect(null).not.toBeOption(); 31 | }); 32 | test('if received is undefined', () => { 33 | expect(undefined).not.toBeOption(); 34 | }); 35 | test('if received is not an Option', () => { 36 | expect('not an Option').not.toBeOption(); 37 | }); 38 | }); 39 | 40 | describe('.not.toBeOption should fail', () => { 41 | test('if received is a none', () => { 42 | expect(() => expect(none).not.toBeOption()).toThrowErrorMatchingInlineSnapshot(` 43 | expect(received).not.toBeOption() 44 | 45 | Received None 46 | `); 47 | }); 48 | test('if received is a some', () => { 49 | expect(() => expect(some('some')).not.toBeOption()).toThrowErrorMatchingInlineSnapshot(` 50 | expect(received).not.toBeOption() 51 | 52 | Received Some: "some" 53 | `); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /src/eitherMatchers/toBeEither.ts: -------------------------------------------------------------------------------- 1 | import { matcherHint, printReceived } from 'jest-matcher-utils'; 2 | import { isEither } from '../predicates'; 3 | import { printReceivedValue } from '../either/print'; 4 | import { Either } from 'fp-ts/lib/Either'; 5 | 6 | const passMessage = (received: Either) => () => 7 | matcherHint('.not.toBeEither', 'received', '') + '\n\n' + `${printReceivedValue(received)}`; 8 | 9 | const failMessage = (received: unknown) => () => 10 | matcherHint('.toBeEither', 'received', '') + '\n\n' + `Received: ${printReceived(received)}`; 11 | 12 | declare global { 13 | namespace jest { 14 | interface Matchers { 15 | /** 16 | * Use `.toBeEither()` to check if a value is consistent with an Either. In other words, this 17 | * matcher confirms that the value is a Left or a Right. 18 | * 19 | * Note that a Left or a Right value is also consistent with a These and would also pass a 20 | * `.toBeThese()` test. 21 | */ 22 | readonly toBeEither: () => R; 23 | } 24 | interface Expect { 25 | /** 26 | * Use `.toBeEither()` to check if a value is consistent with an Either. In other words, this 27 | * matcher confirms that the value is a Left or a Right. 28 | * 29 | * Note that a Left or a Right value is also consistent with a These and would also pass a 30 | * `.toBeThese()` test. 31 | */ 32 | readonly toBeEither: () => any; 33 | } 34 | } 35 | } 36 | 37 | /** 38 | * Check that the received value is an Either 39 | */ 40 | export const toBeEither = (received: unknown): any => { 41 | const pass = isEither(received); 42 | return { 43 | pass, 44 | message: pass ? passMessage(received as Either) : failMessage(received), 45 | }; 46 | }; 47 | -------------------------------------------------------------------------------- /src/theseMatchers/__tests__/toBeBoth.test.ts: -------------------------------------------------------------------------------- 1 | import { left, right, both } from 'fp-ts/lib/These'; 2 | import { toBeBoth } from '../../index'; 3 | 4 | expect.extend({ toBeBoth }); 5 | 6 | describe('.toBeBoth should pass', () => { 7 | test('if received is a Both', () => { 8 | expect(both('Left', 'Right')).toBeBoth(); 9 | }); 10 | test('if called as an asymmetric matcher', () => { 11 | expect(both('Left', 'Right')).toEqual(expect.toBeBoth()); 12 | }); 13 | }); 14 | 15 | describe('.toBeBoth should fail', () => { 16 | test('if received is a right', () => { 17 | expect(() => expect(right('right')).toBeBoth()).toThrowErrorMatchingInlineSnapshot(` 18 | expect(received).toBeBoth() 19 | 20 | Received Right: "right" 21 | `); 22 | }); 23 | test('if received is a left', () => { 24 | expect(() => expect(left('left')).toBeBoth()).toThrowErrorMatchingInlineSnapshot(` 25 | expect(received).toBeBoth() 26 | 27 | Received Left: "left" 28 | `); 29 | }); 30 | test('if received is not a These', () => { 31 | expect(() => expect(null).toBeBoth()).toThrowErrorMatchingInlineSnapshot(` 32 | expect(received).toBeBoth() 33 | 34 | Received value is not a These. 35 | Received: null 36 | `); 37 | }); 38 | }); 39 | 40 | describe('.not.toBeBoth should pass', () => { 41 | test('if received is a Right', () => { 42 | expect(right('Right')).not.toBeBoth(); 43 | }); 44 | test('if received is a Left', () => { 45 | expect(left('Left')).not.toBeBoth(); 46 | }); 47 | test('if received is not a These', () => { 48 | expect(undefined).not.toBeBoth(); 49 | }); 50 | }); 51 | 52 | describe('.not.toBeBoth should fail', () => { 53 | test('if received is a Both', () => { 54 | expect(() => expect(both('left', 'right')).not.toBeBoth()).toThrowErrorMatchingInlineSnapshot(` 55 | expect(received).not.toBeBoth() 56 | 57 | Received Both: 58 | Left: "left" 59 | Right: "right" 60 | `); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /src/validation/validation.ts: -------------------------------------------------------------------------------- 1 | import * as t from 'io-ts'; 2 | import { Either } from 'fp-ts/lib/Either'; 3 | 4 | const leftLiteral = t.literal('Left'); 5 | const rightLiteral = t.literal('Right'); 6 | 7 | const decoder = t.type({ 8 | name: t.string, 9 | // validate: function 10 | // decode: function 11 | }); 12 | 13 | const contextEntry = t.intersection([ 14 | t.type({ 15 | key: t.string, 16 | type: decoder, 17 | }), 18 | t.partial({ 19 | actual: t.unknown, 20 | }), 21 | ]); 22 | 23 | const validationError = t.intersection([ 24 | t.type({ 25 | value: t.unknown, 26 | context: t.readonlyArray(contextEntry), 27 | }), 28 | t.partial({ 29 | message: t.string, 30 | }), 31 | ]); 32 | 33 | const errors = t.array(validationError); 34 | 35 | type ValidationT = t.Type< 36 | Either, t.TypeOf>, 37 | Either, t.OutputOf>, 38 | unknown 39 | >; 40 | 41 | /** 42 | * Given a codec representing a type `A`, returns a codec representing a 43 | * `Validation` that is able to deserialize the JSON representation of a 44 | * `Validation`. 45 | * 46 | * A `Validation` is the type returned by `io-ts decode()` functions in 47 | * classic (stable) io-ts codecs. It is an alias for `Either`. 48 | * 49 | * Note: There is also a new experimental io-ts interface that is independent 50 | * and backward-incompatible with the stable interface. 51 | */ 52 | export const validation = ( 53 | rightCodec: A, 54 | name = `Validation<${rightCodec.name}>`, 55 | ): ValidationT => { 56 | return t.union( 57 | [ 58 | t.strict( 59 | { 60 | _tag: leftLiteral, 61 | left: t.array(validationError), 62 | }, 63 | 'Left', 64 | ), 65 | t.strict( 66 | { 67 | _tag: rightLiteral, 68 | right: rightCodec, 69 | }, 70 | `Right<${rightCodec.name}>`, 71 | ), 72 | ], 73 | name, 74 | ); 75 | }; 76 | -------------------------------------------------------------------------------- /src/these/print.ts: -------------------------------------------------------------------------------- 1 | import { These, fold } from 'fp-ts/lib/These'; 2 | import { printExpected, printDiffOrStringify } from 'jest-matcher-utils'; 3 | import { printReceivedValue } from '../eitherOrThese/print'; 4 | import { equals, strictEquals } from '../predicates/equals'; 5 | 6 | /** 7 | * Construct a string that shows the difference between a received and expected Both value 8 | */ 9 | export function diffReceivedBoth( 10 | received: These, 11 | expectedLeft: unknown, 12 | expectedRight: unknown, 13 | isStrict = false, 14 | ): string { 15 | const predicate = isStrict ? strictEquals : equals; 16 | return fold( 17 | () => 18 | `Expected Both:\n` + 19 | `Left: ${printExpected(expectedLeft)}\n` + 20 | `Right: ${printExpected(expectedRight)}\n` + 21 | '\n' + 22 | printReceivedValue(received), 23 | () => 24 | `Expected Both:\n` + 25 | `Left: ${printExpected(expectedLeft)}\n` + 26 | `Right: ${printExpected(expectedRight)}\n` + 27 | '\n' + 28 | printReceivedValue(received), 29 | (left, right) => { 30 | const diffStringLeft = printDiffOrStringify(expectedLeft, left, 'Expected', 'Received', true); 31 | const diffStringRight = printDiffOrStringify( 32 | expectedRight, 33 | right, 34 | 'Expected', 35 | 'Received', 36 | true, 37 | ); 38 | const leftDiff = !predicate(expectedLeft)(left) 39 | ? '\nDifference from Left:\n' + `${diffStringLeft}` 40 | : ''; 41 | const rightDiff = !predicate(expectedRight)(right) 42 | ? '\nDifference from Right:\n' + `${diffStringRight}` 43 | : ''; 44 | const separator = leftDiff && rightDiff ? '\n' : ''; 45 | const difference = 46 | leftDiff || rightDiff ? leftDiff + separator + rightDiff : '\nNo difference found.'; 47 | return ( 48 | `Expected Both:\n` + 49 | `Left: ${printExpected(expectedLeft)}\n` + 50 | `Right: ${printExpected(expectedRight)}\n` + 51 | difference 52 | ); 53 | }, 54 | )(received); 55 | } 56 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { matchers as decodeMatchers } from './decodeMatchers'; 2 | import { matchers as eitherMatchers } from './eitherMatchers'; 3 | import { matchers as optionMatchers } from './optionMatchers'; 4 | import { matchers as theseMatchers } from './theseMatchers'; 5 | import { matchers as eitherOrTheseMatchers } from './eitherOrTheseMatchers'; 6 | 7 | const matchers = { 8 | ...decodeMatchers, 9 | ...eitherMatchers, 10 | ...optionMatchers, 11 | ...theseMatchers, 12 | ...eitherOrTheseMatchers, 13 | }; 14 | 15 | export { matchers }; 16 | 17 | export { toBeLeftWithErrorsMatching } from './decodeMatchers/toBeLeftWithErrorsMatching'; 18 | export { toBeEither } from './eitherMatchers/toBeEither'; 19 | export { toBeLeft } from './eitherOrTheseMatchers/toBeLeft'; 20 | export { toBeLeftErrorMatching } from './eitherOrTheseMatchers/toBeLeftErrorMatching'; 21 | export { toBeRight } from './eitherOrTheseMatchers/toBeRight'; 22 | export { toEqualLeft } from './eitherOrTheseMatchers/toEqualLeft'; 23 | export { toEqualRight } from './eitherOrTheseMatchers/toEqualRight'; 24 | export { toStrictEqualLeft } from './eitherOrTheseMatchers/toStrictEqualLeft'; 25 | export { toStrictEqualRight } from './eitherOrTheseMatchers/toStrictEqualRight'; 26 | export { toSubsetEqualLeft } from './eitherOrTheseMatchers/toSubsetEqualLeft'; 27 | export { toSubsetEqualRight } from './eitherOrTheseMatchers/toSubsetEqualRight'; 28 | export { toBeOption } from './optionMatchers/toBeOption'; 29 | export { toBeNone } from './optionMatchers/toBeNone'; 30 | export { toBeSome } from './optionMatchers/toBeSome'; 31 | export { toEqualSome } from './optionMatchers/toEqualSome'; 32 | export { toStrictEqualSome } from './optionMatchers/toStrictEqualSome'; 33 | export { toSubsetEqualSome } from './optionMatchers/toSubsetEqualSome'; 34 | export { toBeThese } from './theseMatchers/toBeThese'; 35 | export { toBeBoth } from './theseMatchers/toBeBoth'; 36 | export { toEqualBoth } from './theseMatchers/toEqualBoth'; 37 | export { toStrictEqualBoth } from './theseMatchers/toStrictEqualBoth'; 38 | export { toSubsetEqualBoth } from './theseMatchers/toSubsetEqualBoth'; 39 | 40 | expect.extend(matchers); 41 | -------------------------------------------------------------------------------- /src/theseMatchers/__tests__/toBeThese.test.ts: -------------------------------------------------------------------------------- 1 | import { left, right, both } from 'fp-ts/lib/These'; 2 | import { toBeThese } from '../../index'; 3 | 4 | expect.extend({ toBeThese }); 5 | 6 | describe('.toBeThese should pass', () => { 7 | test('if received is a Left', () => { 8 | expect(left('Left')).toBeThese(); 9 | }); 10 | test('if received is a Right', () => { 11 | expect(right('Right')).toBeThese(); 12 | }); 13 | test('if received is a Both', () => { 14 | expect(both('Left', 'Right')).toBeThese(); 15 | }); 16 | test('if called as an asymmetric matcher', () => { 17 | expect(both('Left', 'Right')).toEqual(expect.toBeThese()); 18 | }); 19 | }); 20 | 21 | describe('.toBeThese should fail', () => { 22 | test('if received is not a left, a right, or a both', () => { 23 | expect(() => expect(undefined).toBeThese()).toThrowErrorMatchingInlineSnapshot(` 24 | expect(received).toBeThese() 25 | 26 | Received: undefined 27 | `); 28 | }); 29 | }); 30 | 31 | describe('.not.toBeThese should pass', () => { 32 | test('if received is null', () => { 33 | expect(null).not.toBeThese(); 34 | }); 35 | test('if received is undefined', () => { 36 | expect(undefined).not.toBeThese(); 37 | }); 38 | test('if received is not a These', () => { 39 | expect(1).not.toBeThese(); 40 | }); 41 | }); 42 | 43 | describe('.not.toBeThese should fail', () => { 44 | test('if received is a left', () => { 45 | expect(() => expect(left('left')).not.toBeThese()).toThrowErrorMatchingInlineSnapshot(` 46 | expect(received).not.toBeThese() 47 | 48 | Received Left: "left" 49 | `); 50 | }); 51 | test('if received is a right', () => { 52 | expect(() => expect(right('right')).not.toBeThese()).toThrowErrorMatchingInlineSnapshot(` 53 | expect(received).not.toBeThese() 54 | 55 | Received Right: "right" 56 | `); 57 | }); 58 | test('if received is a both', () => { 59 | expect(() => expect(both('left', 'right')).not.toBeThese()).toThrowErrorMatchingInlineSnapshot(` 60 | expect(received).not.toBeThese() 61 | 62 | Received Both: 63 | Left: "left" 64 | Right: "right" 65 | `); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Output files 2 | dist/ 3 | 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | lerna-debug.log* 11 | 12 | # Diagnostic reports (https://nodejs.org/api/report.html) 13 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 14 | 15 | # Runtime data 16 | pids 17 | *.pid 18 | *.seed 19 | *.pid.lock 20 | 21 | # Directory for instrumented libs generated by jscoverage/JSCover 22 | lib-cov 23 | 24 | # Coverage directory used by tools like istanbul 25 | coverage 26 | *.lcov 27 | 28 | # nyc test coverage 29 | .nyc_output 30 | 31 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 32 | .grunt 33 | 34 | # Bower dependency directory (https://bower.io/) 35 | bower_components 36 | 37 | # node-waf configuration 38 | .lock-wscript 39 | 40 | # Compiled binary addons (https://nodejs.org/api/addons.html) 41 | build/Release 42 | 43 | # Dependency directories 44 | node_modules/ 45 | jspm_packages/ 46 | 47 | # TypeScript v1 declaration files 48 | typings/ 49 | 50 | # TypeScript cache 51 | *.tsbuildinfo 52 | 53 | # Optional npm cache directory 54 | .npm 55 | 56 | # Optional eslint cache 57 | .eslintcache 58 | 59 | # Microbundle cache 60 | .rpt2_cache/ 61 | .rts2_cache_cjs/ 62 | .rts2_cache_es/ 63 | .rts2_cache_umd/ 64 | 65 | # Optional REPL history 66 | .node_repl_history 67 | 68 | # Output of 'npm pack' 69 | *.tgz 70 | 71 | # Yarn Integrity file 72 | .yarn-integrity 73 | 74 | # dotenv environment variables file 75 | .env 76 | .env.test 77 | 78 | # parcel-bundler cache (https://parceljs.org/) 79 | .cache 80 | 81 | # Next.js build output 82 | .next 83 | 84 | # Nuxt.js build / generate output 85 | .nuxt 86 | dist 87 | 88 | # Gatsby files 89 | .cache/ 90 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 91 | # https://nextjs.org/blog/next-9-1#public-directory-support 92 | # public 93 | 94 | # vuepress build output 95 | .vuepress/dist 96 | 97 | # Serverless directories 98 | .serverless/ 99 | 100 | # FuseBox cache 101 | .fusebox/ 102 | 103 | # DynamoDB Local files 104 | .dynamodb/ 105 | 106 | # TernJS port file 107 | .tern-port 108 | 109 | # Drive upload temp files 110 | **/.tmp.driveupload/ -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true, 4 | "node": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:@typescript-eslint/eslint-recommended", 9 | "plugin:@typescript-eslint/recommended", 10 | "plugin:prettier/recommended" 11 | ], 12 | "globals": { 13 | "Atomics": "readonly", 14 | "SharedArrayBuffer": "readonly" 15 | }, 16 | "parser": "@typescript-eslint/parser", 17 | "parserOptions": { 18 | "ecmaVersion": 2018, 19 | "sourceType": "module" 20 | }, 21 | "plugins": ["@typescript-eslint", "prettier", "import"], 22 | "rules": { 23 | "prettier/prettier": "warn", 24 | "import/no-unresolved": "error", 25 | "@typescript-eslint/explicit-function-return-type": "off", 26 | "@typescript-eslint/no-namespace": "off", 27 | "@typescript-eslint/no-explicit-any": "off", 28 | "@typescript-eslint/member-delimiter-style": [ 29 | "error", 30 | { 31 | "multiline": { 32 | "delimiter": "semi", 33 | "requireLast": true 34 | }, 35 | "singleline": { 36 | "delimiter": "semi", 37 | "requireLast": false 38 | } 39 | } 40 | ], 41 | "@typescript-eslint/semi": ["error", "always"], 42 | "curly": "error", 43 | "eqeqeq": ["error", "always"], 44 | "no-throw-literal": "error" 45 | }, 46 | "overrides": [ 47 | { 48 | "files": ["**/*.test.ts"], 49 | "env": { 50 | "jest": true 51 | }, 52 | "rules": { 53 | "no-unused-vars": "off", 54 | "no-unused-expressions": "off", 55 | "@typescript-eslint/no-explicit-any": "off", 56 | "@typescript-eslint/no-empty-function": "off", 57 | "@typescript-eslint/no-unused-vars": "off" 58 | } 59 | }, 60 | { 61 | "files": ["**/*.js"], 62 | "rules": { 63 | "@typescript-eslint/no-namespace": "off", 64 | "@typescript-eslint/no-var-requires": "off", 65 | "@typescript-eslint/no-explicit-any": "off" 66 | } 67 | } 68 | ], 69 | "settings": { 70 | "import/parsers": { 71 | "@typescript-eslint/parser": [".ts", ".tsx"] 72 | }, 73 | "import/resolver": { 74 | "typescript": { 75 | "alwaysTryTypes": true 76 | } 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/optionMatchers/toEqualSome.ts: -------------------------------------------------------------------------------- 1 | import { matcherHint, printExpected, printReceived } from 'jest-matcher-utils'; 2 | import { applyPredicate } from '../option/applyPredicate'; 3 | import { isOption, equals } from '../predicates'; 4 | import { diffReceivedSome } from '../option/print'; 5 | 6 | const passMessage = (expected: unknown) => () => 7 | matcherHint('.not.toEqualSome', 'received', 'expectedSome') + 8 | '\n\n' + 9 | `Expected Some: not ${printExpected(expected)}`; 10 | 11 | const failMessage = (received: unknown, expected: unknown) => () => { 12 | return isOption(received) 13 | ? matcherHint('.toEqualSome', 'received', 'expectedSome') + 14 | '\n\n' + 15 | diffReceivedSome(received, expected) 16 | : matcherHint('.toEqualSome', 'received', 'expectedSome') + 17 | '\n\n' + 18 | 'Received value is not an Option.\n' + 19 | `Expected Some: ${printExpected(expected)}\n` + 20 | `Received: ${printReceived(received)}`; 21 | }; 22 | 23 | declare global { 24 | namespace jest { 25 | interface Matchers { 26 | /** 27 | * Used to check if a value is a Some that contains a value that equals an expected value. See 28 | * Jest's [toEqual(value)](https://jestjs.io/docs/en/expect#toequalvalue) documentationfor 29 | * information about how the `.toEqual()` comparison works. 30 | */ 31 | readonly toEqualSome: (expected: unknown) => R; 32 | } 33 | interface Expect { 34 | /** 35 | * Used to check if a value is a Some that contains a value that equals an expected value. See 36 | * Jest's [toEqual(value)](https://jestjs.io/docs/en/expect#toequalvalue) documentationfor 37 | * information about how the `.toEqual()` comparison works. 38 | */ 39 | readonly toEqualSome: (expected: unknown) => any; 40 | } 41 | } 42 | } 43 | 44 | /** 45 | * Check that the received value is a Some that equals the expected value 46 | */ 47 | export const toEqualSome = (received: unknown, expected: unknown): any => { 48 | const predicate = equals(expected); 49 | const pass = 50 | isOption(received) && applyPredicate(predicate as (value: unknown) => boolean)(received); 51 | 52 | return { 53 | pass: pass, 54 | message: pass ? passMessage(expected) : failMessage(received, expected), 55 | }; 56 | }; 57 | -------------------------------------------------------------------------------- /src/eitherOrTheseMatchers/toEqualRight.ts: -------------------------------------------------------------------------------- 1 | import { matcherHint, printExpected, printReceived } from 'jest-matcher-utils'; 2 | import { equals, isEitherOrThese } from '../predicates'; 3 | import { applyPredicateRight } from '../eitherOrThese/applyPredicate'; 4 | import { diffReceivedRight } from '../eitherOrThese/print'; 5 | 6 | const passMessage = (expected: unknown) => () => 7 | matcherHint('.not.toEqualRight', 'received', 'expectedRight') + 8 | '\n\n' + 9 | `Expected Right: not ${printExpected(expected)}`; 10 | 11 | const failMessage = (received: unknown, expected: unknown) => () => { 12 | return isEitherOrThese(received) 13 | ? matcherHint('.toEqualRight', 'received', 'expectedRight') + 14 | '\n\n' + 15 | diffReceivedRight(received, expected) 16 | : matcherHint('.toEqualRight', 'received', 'expectedRight') + 17 | '\n\n' + 18 | 'Received value is not an Either or These.\n' + 19 | `Expected Right: ${printExpected(expected)}\n` + 20 | `Received: ${printReceived(received)}`; 21 | }; 22 | 23 | declare global { 24 | namespace jest { 25 | interface Matchers { 26 | /** 27 | * Used to check if a value is a Right whose value equals an expected value. See Jest's 28 | * [toEqual(value)](https://jestjs.io/docs/en/expect#toequalvalue) documentationfor 29 | * information about how the `.toEqual()` comparison works. 30 | */ 31 | readonly toEqualRight: (expected: unknown) => R; 32 | } 33 | interface Expect { 34 | /** 35 | * Used to check if a value is a Right whose value equals an expected value. See Jest's 36 | * [toEqual(value)](https://jestjs.io/docs/en/expect#toequalvalue) documentationfor 37 | * information about how the `.toEqual()` comparison works. 38 | */ 39 | readonly toEqualRight: (expected: unknown) => any; 40 | } 41 | } 42 | } 43 | 44 | /** 45 | * Check that the received value is a Right that equals an expected value 46 | */ 47 | export const toEqualRight = (received: unknown, expected: unknown): any => { 48 | const predicate = equals(expected); 49 | const pass = isEitherOrThese(received) && applyPredicateRight(predicate)(received); 50 | 51 | const message = pass ? passMessage(expected) : failMessage(received, expected); 52 | 53 | return { 54 | pass, 55 | message, 56 | }; 57 | }; 58 | -------------------------------------------------------------------------------- /src/optionMatchers/toStrictEqualSome.ts: -------------------------------------------------------------------------------- 1 | import { matcherHint, printExpected, printReceived } from 'jest-matcher-utils'; 2 | import { applyPredicate } from '../option/applyPredicate'; 3 | import { isOption, strictEquals } from '../predicates'; 4 | import { diffReceivedSome } from '../option/print'; 5 | 6 | const passMessage = (expected: unknown) => () => 7 | matcherHint('.not.toStrictEqualSome', 'received', 'expectedSome') + 8 | '\n\n' + 9 | `Expected Some: not ${printExpected(expected)}`; 10 | 11 | const failMessage = (received: unknown, expected: unknown) => () => { 12 | return isOption(received) 13 | ? matcherHint('.toStrictEqualSome', 'received', 'expectedSome') + 14 | '\n\n' + 15 | diffReceivedSome(received, expected) 16 | : matcherHint('.toStrictEqualSome', 'received', 'expectedSome') + 17 | '\n\n' + 18 | 'Received value is not an Option.\n' + 19 | `Expected Some: ${printExpected(expected)}\n` + 20 | `Received: ${printReceived(received)}`; 21 | }; 22 | 23 | declare global { 24 | namespace jest { 25 | interface Matchers { 26 | /** 27 | * Used to check if a value is a Some that contains a value that strictly equals an expected 28 | * value. See Jest's 29 | * [toStrictEqual(value)](https://jestjs.io/docs/en/expect#tostrictequalvalue) documentation 30 | * for information about how `.toStrictEqual()` differs from `toEqual()`. 31 | */ 32 | readonly toStrictEqualSome: (expected: unknown) => R; 33 | } 34 | interface Expect { 35 | /** 36 | * Used to check if a value is a Some that contains a value that strictly equals an expected 37 | * value. See Jest's 38 | * [toStrictEqual(value)](https://jestjs.io/docs/en/expect#tostrictequalvalue) documentation 39 | * for information about how `.toStrictEqual()` differs from `toEqual()`. 40 | */ 41 | readonly toStrictEqualSome: (expected: unknown) => any; 42 | } 43 | } 44 | } 45 | 46 | /** 47 | * Check that the received value is a Some that strictly equals the expected value 48 | */ 49 | export const toStrictEqualSome = (received: unknown, expected: unknown): any => { 50 | const predicate = strictEquals(expected); 51 | const pass = isOption(received) && applyPredicate(predicate)(received); 52 | 53 | return { 54 | pass: pass, 55 | message: pass ? passMessage(expected) : failMessage(received, expected), 56 | }; 57 | }; 58 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@relmify/jest-fp-ts", 3 | "version": "2.1.1", 4 | "description": "Custom Jest matchers for projects using using fp-ts and io-ts", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "files": [ 8 | "/dist", 9 | "!**/*.map", 10 | "CHANGELOG.md", 11 | "LICENSE", 12 | "README.md" 13 | ], 14 | "keywords": [ 15 | "jest", 16 | "matchers", 17 | "fp-ts", 18 | "io-ts", 19 | "typescript", 20 | "functional" 21 | ], 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/relmify/jest-fp-ts.git" 25 | }, 26 | "author": "Leila Pearson", 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/relmify/jest-fp-ts/issues" 30 | }, 31 | "homepage": "https://github.com/relmify/jest-fp-ts#readme", 32 | "scripts": { 33 | "build": "tsc", 34 | "watch": "tsc -watch", 35 | "clean": "rimraf dist", 36 | "clean:jest": "jest --clear-cache --bail 0", 37 | "lint:check": "eslint ./src --ext .ts", 38 | "lint:fix": "eslint --fix ./src --ext .ts", 39 | "prettier:check": "prettier --check \"src/**/*.ts\"", 40 | "prettier:fix": "prettier --write \"src/**/*.ts\"", 41 | "test": "jest", 42 | "test:watch": "jest --watch", 43 | "prepare": "npm run clean && npm run lint:check && npm run prettier:check && npm test && npm run build" 44 | }, 45 | "dependencies": { 46 | "@jest/expect-utils": "^29.5.0", 47 | "expect": "^29.5.0", 48 | "jest-get-type": "^29.4.3", 49 | "jest-matcher-utils": "^29.5.0" 50 | }, 51 | "peerDependencies": { 52 | "fp-ts": "2.x", 53 | "io-ts": "2.x" 54 | }, 55 | "devDependencies": { 56 | "@relmify/jest-serializer-strip-ansi": "^1.0.2", 57 | "@types/jest": "^29.5.2", 58 | "@types/node": "^18.16.18", 59 | "@typescript-eslint/eslint-plugin": "^5.59.11", 60 | "@typescript-eslint/parser": "^5.59.11", 61 | "eslint": "^8.42.0", 62 | "eslint-config-prettier": "^8.8.0", 63 | "eslint-import-resolver-typescript": "^3.5.5", 64 | "eslint-plugin-import": "^2.25.4", 65 | "eslint-plugin-prettier": "^4.0.0", 66 | "fp-ts": "^2.16.0", 67 | "io-ts": "^2.2.16", 68 | "jest": "^29.5.0", 69 | "jest-snapshot-serializer-raw": "^1.2.0", 70 | "jest-watch-toggle-config": "^3.0.0", 71 | "prettier": "^2.8.8", 72 | "ts-jest": "^29.1.0", 73 | "ts-node": "^10.9.1", 74 | "typescript": "~5.0.4" 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/eitherOrTheseMatchers/toStrictEqualLeft.ts: -------------------------------------------------------------------------------- 1 | import { matcherHint, printExpected, printReceived } from 'jest-matcher-utils'; 2 | import { applyPredicateLeft } from '../eitherOrThese/applyPredicate'; 3 | import { strictEquals, isEitherOrThese } from '../predicates'; 4 | import { diffReceivedLeft } from '../eitherOrThese/print'; 5 | 6 | const passMessage = (expected: unknown) => () => 7 | matcherHint('.not.toStrictEqualLeft', 'received', 'expectedLeft') + 8 | '\n\n' + 9 | `Expected Left: not ${printExpected(expected)}`; 10 | 11 | const failMessage = (received: unknown, expected: unknown) => () => { 12 | return isEitherOrThese(received) 13 | ? matcherHint('.toStrictEqualLeft', 'received', 'expectedLeft') + 14 | '\n\n' + 15 | diffReceivedLeft(received, expected) 16 | : matcherHint('.toStrictEqualLeft', 'received', 'expectedLeft') + 17 | '\n\n' + 18 | 'Received value is not an Either or These.\n' + 19 | `Expected Left: ${printExpected(expected)}\n` + 20 | `Received: ${printReceived(received)}`; 21 | }; 22 | 23 | declare global { 24 | namespace jest { 25 | interface Matchers { 26 | /** 27 | * Used to check if a value is a Left that contains a value that strictly equals an expected 28 | * value. See Jest's 29 | * [toStrictEqual(value)](https://jestjs.io/docs/en/expect#tostrictequalvalue) documentation 30 | * for information about how `.toStrictEqual()` differs from `toEqual()`. 31 | */ 32 | readonly toStrictEqualLeft: (expected: unknown) => R; 33 | } 34 | interface Expect { 35 | /** 36 | * Used to check if a value is a Left that contains a value that strictly equals an expected 37 | * value. See Jest's 38 | * [toStrictEqual(value)](https://jestjs.io/docs/en/expect#tostrictequalvalue) documentation 39 | * for information about how `.toStrictEqual()` differs from `toEqual()`. 40 | */ 41 | readonly toStrictEqualLeft: (expected: unknown) => any; 42 | } 43 | } 44 | } 45 | 46 | /** 47 | * Check that the received value is a Left that strictly equals an expected value 48 | */ 49 | export const toStrictEqualLeft = (received: unknown, expected: unknown): any => { 50 | const predicate = strictEquals(expected); 51 | const pass = isEitherOrThese(received) && applyPredicateLeft(predicate)(received); 52 | 53 | return { 54 | pass: pass, 55 | message: pass ? passMessage(expected) : failMessage(received, expected), 56 | }; 57 | }; 58 | -------------------------------------------------------------------------------- /src/eitherOrTheseMatchers/toStrictEqualRight.ts: -------------------------------------------------------------------------------- 1 | import { matcherHint, printExpected, printReceived } from 'jest-matcher-utils'; 2 | import { applyPredicateRight } from '../eitherOrThese/applyPredicate'; 3 | import { strictEquals, isEitherOrThese } from '../predicates'; 4 | import { diffReceivedRight } from '../eitherOrThese/print'; 5 | 6 | const passMessage = (expected: unknown) => () => 7 | matcherHint('.not.toStrictEqualRight', 'received', 'expectedRight') + 8 | '\n\n' + 9 | `Expected Right: not ${printExpected(expected)}`; 10 | 11 | const failMessage = (received: unknown, expected: unknown) => () => { 12 | return isEitherOrThese(received) 13 | ? matcherHint('.toStrictEqualRight', 'received', 'expectedRight') + 14 | '\n\n' + 15 | diffReceivedRight(received, expected) 16 | : matcherHint('.toStrictEqualRight', 'received', 'expectedRight') + 17 | '\n\n' + 18 | 'Received value is not an Either or These.\n' + 19 | `Expected Right: ${printExpected(expected)}\n` + 20 | `Received: ${printReceived(received)}`; 21 | }; 22 | 23 | declare global { 24 | namespace jest { 25 | interface Matchers { 26 | /** 27 | * Used to check if a value is a Right that contains a value that strictly equals an expected 28 | * value. See Jest's 29 | * [toStrictEqual(value)](https://jestjs.io/docs/en/expect#tostrictequalvalue) documentation 30 | * for information about how `.toStrictEqual()` differs from `toEqual()`. 31 | */ 32 | readonly toStrictEqualRight: (expected: unknown) => R; 33 | } 34 | interface Expect { 35 | /** 36 | * Used to check if a value is a Right that contains a value that strictly equals an expected 37 | * value. See Jest's 38 | * [toStrictEqual(value)](https://jestjs.io/docs/en/expect#tostrictequalvalue) documentation 39 | * for information about how `.toStrictEqual()` differs from `toEqual()`. 40 | */ 41 | readonly toStrictEqualRight: (expected: unknown) => any; 42 | } 43 | } 44 | } 45 | 46 | /** 47 | * Check that the received value is a Right that strictly equals the expected value 48 | */ 49 | export const toStrictEqualRight = (received: unknown, expected: unknown): any => { 50 | const predicate = strictEquals(expected); 51 | const pass = isEitherOrThese(received) && applyPredicateRight(predicate)(received); 52 | 53 | return { 54 | pass: pass, 55 | message: pass ? passMessage(expected) : failMessage(received, expected), 56 | }; 57 | }; 58 | -------------------------------------------------------------------------------- /src/predicates/equals.ts: -------------------------------------------------------------------------------- 1 | import { 2 | equals as jestEquals, 3 | iterableEquality, 4 | typeEquality, 5 | sparseArrayEquality, 6 | arrayBufferEquality, 7 | subsetEquality, 8 | } from '@jest/expect-utils'; 9 | 10 | /** 11 | * Returns `true` if the shape and value of the received object or primitive is equal 12 | * to the shape and value of expected object or primitive. 13 | * 14 | * When called to compare two objects, `equals` performs a deep object comparison. 15 | * All keys must be present in both objects and no extra keys can be present in 16 | * either object. However, the check will still succeed when extra keys with 17 | * undefined values are present. 18 | * 19 | * NOTE: equals does NOT perform a deep compare on two `Error` objects. Only the `message` 20 | * property of an instance of `Error` is considered for equality. Use strictEquals 21 | * to compare more strictly. 22 | */ 23 | export const equals = 24 | (expected: unknown) => 25 | (received: unknown): boolean => 26 | jestEquals(received, expected, [iterableEquality]); 27 | 28 | /** 29 | * Returns `true` if the shape and value of the received object or primitive is equal 30 | * to the shape and value of expected object or primitive. 31 | * 32 | * When called to compare two objects, `equals` performs a deep object comparison. 33 | * All keys must be present in both objects and no extra keys can be present in 34 | * either object. Unlike `equals` a strict equals check will also fail when: 35 | * 36 | * - extra keys with undefined values are present 37 | * - a sparse array is compared to an array with undefined values instead of missing values 38 | * - a class instance is compared to a literal object instance with the same properties 39 | */ 40 | export const strictEquals = 41 | (expected: unknown) => 42 | (received: unknown): boolean => 43 | jestEquals( 44 | received, 45 | expected, 46 | [iterableEquality, typeEquality, sparseArrayEquality, arrayBufferEquality], 47 | true, 48 | ); 49 | 50 | /** 51 | * Returns `true` if a subset of the received object matches the expected object. 52 | * 53 | * If an array is passed, it treats it as an array of objects to match, matching 54 | * each object in the received array against the corresponding object in the 55 | * expected array. If the two arrays are not equal in length it will return `false`. 56 | */ 57 | export const subsetEquals = 58 | (expected: unknown) => 59 | (received: unknown): boolean => 60 | jestEquals(received, expected, [iterableEquality, subsetEquality]); 61 | -------------------------------------------------------------------------------- /src/eitherOrTheseMatchers/toEqualLeft.ts: -------------------------------------------------------------------------------- 1 | import { matcherHint, printExpected, printReceived, stringify } from 'jest-matcher-utils'; 2 | import { equals, isEitherOrThese } from '../predicates'; 3 | import { applyPredicateLeft } from '../eitherOrThese/applyPredicate'; 4 | import { diffReceivedLeft, printReceivedValue } from '../eitherOrThese/print'; 5 | import { EitherOrThese } from '../eitherOrThese/eitherOrThese'; 6 | import { left } from 'fp-ts/lib/Either'; 7 | 8 | const passMessage = (received: EitherOrThese, expected: unknown) => () => 9 | matcherHint('.not.toEqualLeft', 'received', 'expectedLeft') + 10 | '\n\n' + 11 | `Expected Left: not ${printExpected(expected)}` + 12 | (stringify(left(expected)) !== stringify(received) ? `${printReceivedValue(received)}` : ''); 13 | 14 | const failMessage = (received: unknown, expected: unknown) => () => { 15 | return isEitherOrThese(received) 16 | ? matcherHint('.toEqualLeft', 'received', 'expectedLeft') + 17 | '\n\n' + 18 | diffReceivedLeft(received, expected) 19 | : matcherHint('.toEqualLeft', 'received', 'expectedLeft') + 20 | '\n\n' + 21 | 'Received value is not an Either or These.\n' + 22 | `Expected Left: ${printExpected(expected)}\n` + 23 | `Received: ${printReceived(received)}`; 24 | }; 25 | 26 | declare global { 27 | namespace jest { 28 | interface Matchers { 29 | /** 30 | * Used to check if a value is a Left whose value equals an expected 31 | * value. See Jest's [toEqual(value)](https://jestjs.io/docs/en/expect#toequalvalue) 32 | * documentation for information about how the `.toEqual()` comparison works. 33 | */ 34 | readonly toEqualLeft: (expected: unknown) => R; 35 | } 36 | 37 | interface Expect { 38 | /** 39 | * Used to check if a value is a Left whose value equals an expected 40 | * value. See Jest's [toEqual(value)](https://jestjs.io/docs/en/expect#toequalvalue) 41 | * documentation for information about how the `.toEqual()` comparison works. 42 | */ 43 | readonly toEqualLeft: (expected: unknown) => any; 44 | } 45 | } 46 | } 47 | 48 | /** 49 | * Check that the received value is a Left that equals an expected value 50 | */ 51 | export const toEqualLeft = (received: unknown, expected: unknown): any => { 52 | const predicate = equals(expected); 53 | const pass = isEitherOrThese(received) && applyPredicateLeft(predicate)(received); 54 | 55 | const message = pass ? passMessage(received, expected) : failMessage(received, expected); 56 | 57 | return { 58 | pass, 59 | message, 60 | }; 61 | }; 62 | -------------------------------------------------------------------------------- /src/theseMatchers/toEqualBoth.ts: -------------------------------------------------------------------------------- 1 | import { matcherHint, printExpected, printReceived } from 'jest-matcher-utils'; 2 | import { equals, isThese } from '../predicates'; 3 | import { applyPredicateBoth } from '../these/applyPredicate'; 4 | import { diffReceivedBoth } from '../these/print'; 5 | 6 | const passMessage = (expectedLeft: unknown, expectedRight: unknown) => () => 7 | matcherHint('.not.toEqualBoth', 'received', 'expectedBoth') + 8 | '\n\n' + 9 | `Expected Both: not \n` + 10 | ` Left: ${printExpected(expectedLeft)}` + 11 | ` Right: ${printExpected(expectedRight)}`; 12 | 13 | const failMessage = (received: unknown, expectedLeft: unknown, expectedRight: unknown) => () => { 14 | return isThese(received) 15 | ? matcherHint('.toEqualBoth', 'received', 'expectedLeft, expectedRight') + 16 | '\n\n' + 17 | diffReceivedBoth(received, expectedLeft, expectedRight) 18 | : matcherHint('.toEqualBoth', 'received', 'expectedLeft, expectedRight') + 19 | '\n\n' + 20 | 'Received value is not a These.\n' + 21 | `Expected Both:\n` + 22 | ` Left: ${printExpected(expectedLeft)}\n` + 23 | ` Right: ${printExpected(expectedRight)}\n` + 24 | `Received: ${printReceived(received)}`; 25 | }; 26 | 27 | declare global { 28 | namespace jest { 29 | interface Matchers { 30 | /** 31 | * Use `.toEqualBoth(leftValue, rightValue)` to check if a value is a Both that contains a 32 | * left value that equals an expected value, and a right value that equals an expected value. 33 | * See Jest's [toEqual(value)](https://jestjs.io/docs/en/expect#toequalvalue) documentationfor 34 | * information about how the `.toEqual()` comparison works. 35 | */ 36 | readonly toEqualBoth: (expectedLeft: unknown, expectedRight: unknown) => R; 37 | } 38 | interface Expect { 39 | /** 40 | * Used to check if a value is a Both that contains a left value that equals an expected 41 | * value, and a right value that equals an expected value. See Jest's 42 | * [toEqual(value)](https://jestjs.io/docs/en/expect#toequalvalue) documentationfor 43 | * information about how the `.toEqual()` comparison works. 44 | */ 45 | readonly toEqualBoth: (expectedLeft: unknown, expectedRight: unknown) => any; 46 | } 47 | } 48 | } 49 | 50 | /** 51 | * Check that the received value is a Both with left and right values that equal the expected 52 | * left and right values. 53 | */ 54 | export const toEqualBoth = ( 55 | received: unknown, 56 | expectedLeft: unknown, 57 | expectedRight: unknown, 58 | ): any => { 59 | const predicateLeft = equals(expectedLeft); 60 | const predicateRight = equals(expectedRight); 61 | const pass = isThese(received) && applyPredicateBoth(predicateLeft, predicateRight)(received); 62 | 63 | const message = pass 64 | ? passMessage(expectedLeft, expectedRight) 65 | : failMessage(received, expectedLeft, expectedRight); 66 | 67 | return { 68 | pass, 69 | message, 70 | }; 71 | }; 72 | -------------------------------------------------------------------------------- /src/theseMatchers/toStrictEqualBoth.ts: -------------------------------------------------------------------------------- 1 | import { matcherHint, printExpected, printReceived } from 'jest-matcher-utils'; 2 | import { strictEquals, isThese } from '../predicates'; 3 | import { applyPredicateBoth } from '../these/applyPredicate'; 4 | import { diffReceivedBoth } from '../these/print'; 5 | 6 | const passMessage = (expectedLeft: unknown, expectedRight: unknown) => () => 7 | matcherHint('.not.toStrictEqualBoth', 'received', 'expectedBoth') + 8 | '\n\n' + 9 | `Expected Both: not \n` + 10 | ` Left: ${printExpected(expectedLeft)}\n` + 11 | ` Right: ${printExpected(expectedRight)}`; 12 | 13 | const failMessage = (received: unknown, expectedLeft: unknown, expectedRight: unknown) => () => { 14 | return isThese(received) 15 | ? matcherHint('.toStrictEqualBoth', 'received', 'expectedLeft, expectedRight') + 16 | '\n\n' + 17 | diffReceivedBoth(received, expectedLeft, expectedRight, true) 18 | : matcherHint('.toStrictEqualBoth', 'received', 'expectedLeft, expectedRight') + 19 | '\n\n' + 20 | 'Received value is not a These.\n' + 21 | `Expected Both:\n` + 22 | ` Left: ${printExpected(expectedLeft)}\n` + 23 | ` Right: ${printExpected(expectedRight)}\n` + 24 | `Received: ${printReceived(received)}`; 25 | }; 26 | 27 | declare global { 28 | namespace jest { 29 | interface Matchers { 30 | /** 31 | * Used to check if a value is a Both that contains a Left value that strictly equals an 32 | * expected value, and a Right value that strictly equals an expected value. See Jest's 33 | * [toStrictEqual(value)](https://jestjs.io/docs/en/expect#tostrictequalvalue) documentation 34 | * for information about how `.toStrictEqual()` differs from `toEqual()`. 35 | */ 36 | readonly toStrictEqualBoth: (expectedLeft: unknown, expectedRight: unknown) => R; 37 | } 38 | interface Expect { 39 | /** 40 | * Used to check if a value is a Both that contains a Left value that strictly equals an 41 | * expected value, and a Right value that strictly equals an expected value. See Jest's 42 | * [toStrictEqual(value)](https://jestjs.io/docs/en/expect#tostrictequalvalue) documentation 43 | * for information about how `.toStrictEqual()` differs from `toEqual()`. 44 | */ 45 | readonly toStrictEqualBoth: (expectedLeft: unknown, expectedRight: unknown) => any; 46 | } 47 | } 48 | } 49 | 50 | /** 51 | * Check that the received value is a Both with left and right values that strictly equal the 52 | * expected left and right values. 53 | */ 54 | export const toStrictEqualBoth = ( 55 | received: unknown, 56 | expectedLeft: unknown, 57 | expectedRight: unknown, 58 | ): any => { 59 | const predicateLeft = strictEquals(expectedLeft); 60 | const predicateRight = strictEquals(expectedRight); 61 | const pass = isThese(received) && applyPredicateBoth(predicateLeft, predicateRight)(received); 62 | 63 | const message = pass 64 | ? passMessage(expectedLeft, expectedRight) 65 | : failMessage(received, expectedLeft, expectedRight); 66 | 67 | return { 68 | pass, 69 | message, 70 | }; 71 | }; 72 | -------------------------------------------------------------------------------- /src/eitherMatchers/__tests__/toBeEither.test.ts: -------------------------------------------------------------------------------- 1 | import { left, right } from 'fp-ts/lib/Either'; 2 | import { left as leftThese, right as rightThese, both } from 'fp-ts/lib/These'; 3 | import { toBeEither } from '../../index'; 4 | 5 | expect.extend({ toBeEither }); 6 | 7 | describe('.toBeEither should pass', () => { 8 | test('if received is a Left', () => { 9 | expect(left('Left')).toBeEither(); 10 | }); 11 | test('if received is a Right', () => { 12 | expect(right('Right')).toBeEither(); 13 | }); 14 | // cannot distinguish left These from left Either 15 | test('if received is a Left These', () => { 16 | expect(leftThese('LeftThese')).toBeEither(); 17 | }); 18 | // cannot distinguish right These from right Either 19 | test('if received is a Right These', () => { 20 | expect(rightThese('RightThese')).toBeEither(); 21 | }); 22 | test('if called as an asymmetric matcher', () => { 23 | expect(left('Left')).toEqual(expect.toBeEither()); 24 | }); 25 | }); 26 | 27 | describe('.toBeEither should fail', () => { 28 | test('if received is neither a left nor a right', () => { 29 | expect(() => expect(1).toBeEither()).toThrowErrorMatchingInlineSnapshot(` 30 | expect(received).toBeEither() 31 | 32 | Received: 1 33 | `); 34 | }); 35 | test('if received is a both', () => { 36 | expect(() => expect(both('left', 'right')).toBeEither()).toThrowErrorMatchingInlineSnapshot(` 37 | expect(received).toBeEither() 38 | 39 | Received: {"_tag": "Both", "left": "left", "right": "right"} 40 | `); 41 | }); 42 | }); 43 | 44 | describe('.not.toBeEither should pass', () => { 45 | test('if received is null', () => { 46 | expect(null).not.toBeEither(); 47 | }); 48 | test('if received is undefined', () => { 49 | expect(undefined).not.toBeEither(); 50 | }); 51 | test('if received is a Both', () => { 52 | expect(both('left', 'right')).not.toBeEither(); 53 | }); 54 | test('if received is a string', () => { 55 | expect('not an Either').not.toBeEither(); 56 | }); 57 | }); 58 | 59 | describe('.not.toBeEither should fail', () => { 60 | test('if received is a left', () => { 61 | expect(() => expect(left('left')).not.toBeEither()).toThrowErrorMatchingInlineSnapshot(` 62 | expect(received).not.toBeEither() 63 | 64 | Received Left: "left" 65 | `); 66 | }); 67 | test('if received is a right', () => { 68 | expect(() => expect(right('right')).not.toBeEither()).toThrowErrorMatchingInlineSnapshot(` 69 | expect(received).not.toBeEither() 70 | 71 | Received Right: "right" 72 | `); 73 | }); 74 | // cannot distinguish left These from left Either 75 | test('if received is a left These', () => { 76 | expect(() => expect(leftThese('left')).not.toBeEither()).toThrowErrorMatchingInlineSnapshot(` 77 | expect(received).not.toBeEither() 78 | 79 | Received Left: "left" 80 | `); 81 | }); 82 | // cannot distinguish right These from right Either 83 | test('if received is a right These', () => { 84 | expect(() => expect(rightThese('right')).not.toBeEither()).toThrowErrorMatchingInlineSnapshot(` 85 | expect(received).not.toBeEither() 86 | 87 | Received Right: "right" 88 | `); 89 | }); 90 | }); 91 | -------------------------------------------------------------------------------- /src/optionMatchers/__tests__/toEqualSome.test.ts: -------------------------------------------------------------------------------- 1 | import { some, none } from 'fp-ts/lib/Option'; 2 | import { toEqualSome } from '../../index'; 3 | 4 | expect.extend({ toEqualSome }); 5 | 6 | describe('.toEqualSome should pass', () => { 7 | test('if received is a Some with the expected value', () => { 8 | expect(some('Some')).toEqualSome('Some'); 9 | }); 10 | test('if received is a Some with an undefined expected value', () => { 11 | expect(some(undefined)).toEqualSome(undefined); 12 | }); 13 | test('if the received is a Some with a null expected value', () => { 14 | expect(some(null)).toEqualSome(null); 15 | }); 16 | test('if called as an asymmetric matcher', () => { 17 | expect(some('Any sufficiently advanced technology is equivalent to magic.')).toEqual( 18 | expect.toEqualSome('Any sufficiently advanced technology is equivalent to magic.'), 19 | ); 20 | }); 21 | test('if called with an asymmetric matcher', () => { 22 | expect(some('You affect the world by what you browse.')).toEqualSome( 23 | expect.stringContaining('world'), 24 | ); 25 | }); 26 | }); 27 | 28 | describe('.toEqualSome should fail', () => { 29 | test('if received is a None', () => { 30 | expect(() => expect(none).toEqualSome('Some')).toThrowErrorMatchingInlineSnapshot(` 31 | expect(received).toEqualSome(expectedSome) 32 | 33 | Expected Some: "Some" 34 | Received None 35 | `); 36 | }); 37 | test('if received is a Some that does not equal the expected value', () => { 38 | expect(() => expect(some('another Some')).toEqualSome('Some')) 39 | .toThrowErrorMatchingInlineSnapshot(` 40 | expect(received).toEqualSome(expectedSome) 41 | 42 | Expected Some: "Some" 43 | Received Some: "another Some" 44 | `); 45 | }); 46 | test('if received is a Some with a number value that does not equal the expected value', () => { 47 | expect(() => expect(some(1)).toEqualSome(2)).toThrowErrorMatchingInlineSnapshot(` 48 | expect(received).toEqualSome(expectedSome) 49 | 50 | Expected Some: 2 51 | Received Some: 1 52 | `); 53 | }); 54 | test('if received value is not an Option', () => { 55 | expect(() => expect(undefined).toEqualSome(undefined)).toThrowErrorMatchingInlineSnapshot(` 56 | expect(received).toEqualSome(expectedSome) 57 | 58 | Received value is not an Option. 59 | Expected Some: undefined 60 | Received: undefined 61 | `); 62 | }); 63 | }); 64 | 65 | describe('.not.toEqualSome should pass', () => { 66 | test('if received is a Some that does not have the expected value', () => { 67 | expect(some('Some')).not.toEqualSome('A different value'); 68 | }); 69 | test('if received is a None', () => { 70 | expect(none).not.toEqualSome('Some'); 71 | }); 72 | test('if received value is not an Option', () => { 73 | expect(1).not.toEqualSome(1); 74 | }); 75 | }); 76 | 77 | describe('.not.toEqualSome should fail', () => { 78 | test('if received is a Some that equals the expected value', () => { 79 | expect(() => expect(some('Some')).not.toEqualSome('Some')).toThrowErrorMatchingInlineSnapshot(` 80 | expect(received).not.toEqualSome(expectedSome) 81 | 82 | Expected Some: not "Some" 83 | `); 84 | }); 85 | }); 86 | -------------------------------------------------------------------------------- /src/eitherOrTheseMatchers/toBeLeftErrorMatching.ts: -------------------------------------------------------------------------------- 1 | import { matcherHint, printExpected, printReceived } from 'jest-matcher-utils'; 2 | import { isEitherOrThese, isError, matches, equals } from '../predicates'; 3 | import { isLeft } from '../eitherOrThese/eitherOrThese'; 4 | import { EitherOrThese } from '../eitherOrThese/eitherOrThese'; 5 | import { printReceivedValue, printReceivedLeftErrorValue } from '../eitherOrThese/print'; 6 | import { left } from 'fp-ts/lib/Either'; 7 | 8 | const passMessage = 9 | (received: EitherOrThese, expected: string | RegExp) => () => { 10 | const hint = 11 | matcherHint('.not.toBeLeftErrorMatching', 'received', 'expectedErrorMessage') + '\n\n'; 12 | const expectedString = typeof expected === 'string' ? expected : undefined; 13 | const expectedValue = `Expected Left Error: not ${printExpected(expected)}`; 14 | const receivedValue = equals(left(new Error(expectedString)))(received) 15 | ? '' 16 | : `\n${printReceivedLeftErrorValue(received)}`; 17 | return hint + expectedValue + receivedValue; 18 | }; 19 | 20 | const failMessage = (received: unknown, expected: string | RegExp) => () => { 21 | const hint = matcherHint('.toBeLeftErrorMatching', 'received', 'expectedErrorMessage'); 22 | const expectedValue = `Expected Left Error: ${printExpected(expected)}\n`; 23 | return isEitherOrThese(received) 24 | ? isLeft(received) 25 | ? isError(received.left) 26 | ? hint + 27 | '\n\n' + 28 | expectedValue + 29 | `Received Left Error: ${printReceived(received.left.message)}` 30 | : hint + 31 | '\n\n' + 32 | `Received Left value is not an Error.\n` + 33 | expectedValue + 34 | `${printReceivedValue(received)}` 35 | : hint + 36 | '\n\n' + 37 | `Received value is not a Left.\n` + 38 | expectedValue + 39 | `${printReceivedValue(received)}` 40 | : hint + 41 | '\n\n' + 42 | 'Received value is not an Either or These.\n' + 43 | expectedValue + 44 | `Received: ${printReceived(received)}`; 45 | }; 46 | 47 | declare global { 48 | namespace jest { 49 | interface Matchers { 50 | /** 51 | * Used to check if a value is a Left Error whose `message` property contains the supplied 52 | * string or matches the supplied regular expression. 53 | */ 54 | readonly toBeLeftErrorMatching: (expected: string | RegExp) => R; 55 | } 56 | interface Expect { 57 | /** 58 | * Used to check if a value is a Left Error whose `message` property contains the supplied 59 | * string or matches the supplied regular expression. 60 | */ 61 | readonly toBeLeftErrorMatching: (expected: string | RegExp) => any; 62 | } 63 | } 64 | } 65 | 66 | /** 67 | * Check that the received value is a Left Error whose `message` property contains the supplied 68 | * string or matches the supplied regular expression. 69 | */ 70 | export const toBeLeftErrorMatching = (received: unknown, expected: string | RegExp): any => { 71 | const pass = 72 | isEitherOrThese(received) && 73 | isLeft(received) && 74 | isError(received.left) && 75 | matches(received.left.message, expected); 76 | return { 77 | pass, 78 | message: pass ? passMessage(received, expected) : failMessage(received, expected), 79 | }; 80 | }; 81 | -------------------------------------------------------------------------------- /src/option/__tests__/print.test.ts: -------------------------------------------------------------------------------- 1 | import { some, none } from 'fp-ts/lib/Option'; 2 | import { printReceivedOption, diffReceivedSome } from '../print'; 3 | 4 | describe('printReceivedOption', () => { 5 | test('includes the received value if a Some with a string value is received', () => { 6 | const aSome = some('A Some with a string value'); 7 | expect(printReceivedOption(aSome)).toMatchInlineSnapshot( 8 | `Received Some: "A Some with a string value"`, 9 | ); 10 | }); 11 | test('includes the received value if a Some with an Error value is received', () => { 12 | const errorSome = some(new Error('A Some with an Error value')); 13 | expect(printReceivedOption(errorSome)).toMatchInlineSnapshot( 14 | `Received Some: [Error: A Some with an Error value]`, 15 | ); 16 | }); 17 | test('includes the received value if a Some with an object value is received', () => { 18 | expect(printReceivedOption(some({ id: 1 }))).toMatchInlineSnapshot(`Received Some: {"id": 1}`); 19 | }); 20 | test('indicates if the received value is a None', () => { 21 | expect(printReceivedOption(none)).toMatchInlineSnapshot(`Received None`); 22 | }); 23 | test('handles a Some with an undefined value', () => { 24 | expect(printReceivedOption(some(undefined))).toMatchInlineSnapshot(`Received Some: undefined`); 25 | }); 26 | test('handles a Some with a null value', () => { 27 | expect(printReceivedOption(some(null))).toMatchInlineSnapshot(`Received Some: null`); 28 | }); 29 | }); 30 | 31 | describe('printReceivedOption adds padding', () => { 32 | test('if a Some is received and addPadding is true', () => { 33 | expect(printReceivedOption(some(1), true)).toMatchInlineSnapshot(`Received Some: 1`); 34 | }); 35 | }); 36 | 37 | describe('printReceivedOption does not add padding', () => { 38 | test('if a Some is received and addPadding is true', () => { 39 | expect(printReceivedOption(some(1), false)).toMatchInlineSnapshot(`Received Some: 1`); 40 | }); 41 | test('if a None is received and addPadding is true', () => { 42 | expect(printReceivedOption(none, true)).toEqual(printReceivedOption(none)); 43 | }); 44 | }); 45 | 46 | describe('diffReceivedSome', () => { 47 | test('returns a string that contains the expected and received values', () => { 48 | expect(diffReceivedSome(some('A Some'), 'A Some with a different value')) 49 | .toMatchInlineSnapshot(` 50 | Expected Some: "A Some with a different value" 51 | Received Some: "A Some" 52 | `); 53 | }); 54 | test('indicates if a None was recieved', () => { 55 | expect(diffReceivedSome(none, 'A Some with some value')).toMatchInlineSnapshot(` 56 | Expected Some: "A Some with some value" 57 | Received None 58 | `); 59 | }); 60 | test('handles null values', () => { 61 | expect(diffReceivedSome(some(null), 'A Some with some value')).toMatchInlineSnapshot(` 62 | Expected Some: "A Some with some value" 63 | Received Some: null 64 | `); 65 | }); 66 | test('handles number values', () => { 67 | expect(diffReceivedSome(some(42), 21)).toMatchInlineSnapshot(` 68 | Expected Some: 21 69 | Received Some: 42 70 | `); 71 | }); 72 | test('handles asymetric matcher values', () => { 73 | expect(diffReceivedSome(some(42), expect.any(String))).toMatchInlineSnapshot(` 74 | Expected Some: Any 75 | Received Some: 42 76 | `); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /src/eitherOrTheseMatchers/__tests__/toBeLeft.test.ts: -------------------------------------------------------------------------------- 1 | import { left as leftEither, right as rightEither } from 'fp-ts/lib/Either'; 2 | import { left as leftThese, right as rightThese, both } from 'fp-ts/lib/These'; 3 | import { some, none } from 'fp-ts/lib/Option'; 4 | import { toBeLeft } from '../../index'; 5 | 6 | expect.extend({ toBeLeft }); 7 | 8 | describe('.toBeLeft should pass', () => { 9 | test('if received is a Left Either', () => { 10 | expect(leftEither('Left')).toBeLeft(); 11 | }); 12 | test('if received is a Left These', () => { 13 | expect(leftThese('Left')).toBeLeft(); 14 | }); 15 | test('if called as an asymmetric matcher', () => { 16 | expect(leftThese('Left')).toEqual(expect.toBeLeft()); 17 | }); 18 | }); 19 | 20 | describe('.toBeLeft should fail', () => { 21 | test('if received is a Right Either', () => { 22 | expect(() => expect(rightEither('right')).toBeLeft()).toThrowErrorMatchingInlineSnapshot(` 23 | expect(received).toBeLeft() 24 | 25 | Received Right: "right" 26 | `); 27 | }); 28 | test('if received is a Right These', () => { 29 | expect(() => expect(rightThese('right')).toBeLeft()).toThrowErrorMatchingInlineSnapshot(` 30 | expect(received).toBeLeft() 31 | 32 | Received Right: "right" 33 | `); 34 | }); 35 | test('if received is a Both', () => { 36 | expect(() => expect(both('left', 'right')).toBeLeft()).toThrowErrorMatchingInlineSnapshot(` 37 | expect(received).toBeLeft() 38 | 39 | Received Both: 40 | Left: "left" 41 | Right: "right" 42 | `); 43 | }); 44 | test('if received is a Some', () => { 45 | expect(() => expect(some(1)).toBeLeft()).toThrowErrorMatchingInlineSnapshot(` 46 | expect(received).toBeLeft() 47 | 48 | Received value is not an Either or These. 49 | Received: {"_tag": "Some", "value": 1} 50 | `); 51 | }); 52 | test('if received is a basic type', () => { 53 | expect(() => expect(42).toBeLeft()).toThrowErrorMatchingInlineSnapshot(` 54 | expect(received).toBeLeft() 55 | 56 | Received value is not an Either or These. 57 | Received: 42 58 | `); 59 | }); 60 | test('if received is undefined', () => { 61 | expect(() => expect(undefined).toBeLeft()).toThrowErrorMatchingInlineSnapshot(` 62 | expect(received).toBeLeft() 63 | 64 | Received value is not an Either or These. 65 | Received: undefined 66 | `); 67 | }); 68 | }); 69 | 70 | describe('.not.toBeLeft should pass', () => { 71 | test('if received is a Right Either', () => { 72 | expect(rightEither('Right')).not.toBeLeft(); 73 | }); 74 | test('if received is a Right These', () => { 75 | expect(rightThese('Right')).not.toBeLeft(); 76 | }); 77 | test('if received is a Both', () => { 78 | expect(both('Left', 'Right')).not.toBeLeft(); 79 | }); 80 | test('if received is a None', () => { 81 | expect(none).not.toBeLeft(); 82 | }); 83 | test('if received is a basic type', () => { 84 | expect('a').not.toBeLeft(); 85 | }); 86 | test('if received is null', () => { 87 | expect(null).not.toBeLeft(); 88 | }); 89 | }); 90 | 91 | describe('.not.toBeLeft should fail', () => { 92 | test('if received is a left Either', () => { 93 | expect(() => expect(leftEither('left')).not.toBeLeft()).toThrowErrorMatchingInlineSnapshot(` 94 | expect(received).not.toBeLeft() 95 | 96 | Received Left: "left" 97 | `); 98 | }); 99 | test('if received is a left These', () => { 100 | expect(() => expect(leftThese('left')).not.toBeLeft()).toThrowErrorMatchingInlineSnapshot(` 101 | expect(received).not.toBeLeft() 102 | 103 | Received Left: "left" 104 | `); 105 | }); 106 | }); 107 | -------------------------------------------------------------------------------- /src/eitherOrTheseMatchers/__tests__/toBeRight.test.ts: -------------------------------------------------------------------------------- 1 | import { left as leftEither, right as rightEither } from 'fp-ts/lib/Either'; 2 | import { left as leftThese, right as rightThese, both } from 'fp-ts/lib/These'; 3 | import { none, some } from 'fp-ts/lib/Option'; 4 | import { toBeRight } from '../../index'; 5 | 6 | expect.extend({ toBeRight }); 7 | 8 | describe('.toBeRight', () => { 9 | test('should pass if the received object is a Right Either', () => { 10 | expect(rightEither('Right')).toBeRight(); 11 | }); 12 | test('should pass if the received object is a Right These', () => { 13 | expect(rightThese('Right')).toBeRight(); 14 | }); 15 | test('if called as an asymmetric matcher', () => { 16 | expect(rightEither('Right')).toEqual(expect.toBeRight()); 17 | }); 18 | }); 19 | 20 | describe('.toBeRight should fail', () => { 21 | test('if received is a Left Either', () => { 22 | expect(() => expect(leftEither('left')).toBeRight()).toThrowErrorMatchingInlineSnapshot(` 23 | expect(received).toBeRight() 24 | 25 | Received Left: "left" 26 | `); 27 | }); 28 | test('if received is a Left These', () => { 29 | expect(() => expect(leftThese('left')).toBeRight()).toThrowErrorMatchingInlineSnapshot(` 30 | expect(received).toBeRight() 31 | 32 | Received Left: "left" 33 | `); 34 | }); 35 | test('if received is a Both', () => { 36 | expect(() => expect(both('left', 'right')).toBeRight()).toThrowErrorMatchingInlineSnapshot(` 37 | expect(received).toBeRight() 38 | 39 | Received Both: 40 | Left: "left" 41 | Right: "right" 42 | `); 43 | }); 44 | test('if received is a None', () => { 45 | expect(() => expect(none).toBeRight()).toThrowErrorMatchingInlineSnapshot(` 46 | expect(received).toBeRight() 47 | 48 | Received value is not an Either or These. 49 | Received: {"_tag": "None"} 50 | `); 51 | }); 52 | test('if received is a basic type', () => { 53 | expect(() => expect('a').toBeRight()).toThrowErrorMatchingInlineSnapshot(` 54 | expect(received).toBeRight() 55 | 56 | Received value is not an Either or These. 57 | Received: "a" 58 | `); 59 | }); 60 | test('if received is null', () => { 61 | expect(() => expect(null).toBeRight()).toThrowErrorMatchingInlineSnapshot(` 62 | expect(received).toBeRight() 63 | 64 | Received value is not an Either or These. 65 | Received: null 66 | `); 67 | }); 68 | }); 69 | 70 | describe('not.toBeRight should pass', () => { 71 | test('if the received is a Left Either', () => { 72 | expect(leftEither('Left')).not.toBeRight(); 73 | }); 74 | test('if the received is a Left These', () => { 75 | expect(leftThese('Left')).not.toBeRight(); 76 | }); 77 | test('if the received is a Both', () => { 78 | expect(both('Left', 'Right')).not.toBeRight(); 79 | }); 80 | test('if received is a Some', () => { 81 | expect(some(1)).not.toBeRight(); 82 | }); 83 | test('if received is a basic type', () => { 84 | expect(0).not.toBeRight(); 85 | }); 86 | test('if received is undefined', () => { 87 | expect(undefined).not.toBeRight(); 88 | }); 89 | }); 90 | 91 | describe('.not.toBeRight should fail', () => { 92 | test('if received is a Right Either', () => { 93 | expect(() => expect(rightEither('right')).not.toBeRight()).toThrowErrorMatchingInlineSnapshot(` 94 | expect(received).not.toBeRight() 95 | 96 | Received Right: "right" 97 | `); 98 | }); 99 | test('if received is a Right These', () => { 100 | expect(() => expect(rightThese('right')).not.toBeRight()).toThrowErrorMatchingInlineSnapshot(` 101 | expect(received).not.toBeRight() 102 | 103 | Received Right: "right" 104 | `); 105 | }); 106 | }); 107 | -------------------------------------------------------------------------------- /src/eitherOrThese/print.ts: -------------------------------------------------------------------------------- 1 | import { EitherOrThese, fold } from './eitherOrThese'; 2 | import { isError } from '../predicates'; 3 | import { printExpected, printReceived, printDiffOrStringify } from 'jest-matcher-utils'; 4 | 5 | /** 6 | * Construct a string that shows the received Either or These value. 7 | * 8 | * Optionally add padding to align with a previous `Expected Left: not ` 9 | * or `Expected Right: not` 10 | */ 11 | export function printReceivedValue( 12 | received: EitherOrThese, 13 | addPadding = false, 14 | ): string { 15 | const padding = addPadding ? ' ' : ''; 16 | return fold( 17 | (left) => `Received Left: ` + padding + `${printReceived(left)}`, 18 | (right) => `Received Right: ` + padding + `${printReceived(right)}`, 19 | (left, right) => 20 | `Received Both:\n` + `Left: ${printReceived(left)}\n` + `Right: ${printReceived(right)}`, 21 | )(received); 22 | } 23 | 24 | /** 25 | * Construct a string that either shows the received Left value or indicates that a Right or Both 26 | * was received. 27 | */ 28 | export function printReceivedLeft( 29 | received: EitherOrThese, 30 | addPadding = false, 31 | ): string { 32 | const padding = addPadding ? ' ' : ''; 33 | return fold( 34 | (left) => `Received Left: ` + padding + `${printReceived(left)}`, 35 | () => `Received a Right.`, 36 | () => `Received a Both.`, 37 | )(received); 38 | } 39 | 40 | /** 41 | * Construct a string that shows the received Either or These Left Error value or 42 | * prints what was actually received if not a Left Error. 43 | * 44 | * Optionally add padding to align with a previous `Expected Left Error: not ` 45 | */ 46 | export function printReceivedLeftErrorValue( 47 | received: EitherOrThese, 48 | addPadding = false, 49 | ): string { 50 | const padding = addPadding ? ' ' : ''; 51 | return fold( 52 | (left) => 53 | isError(left) 54 | ? `Received Left Error: ` + padding + `${printReceived(left.message)}` 55 | : `Received Left: ` + `${printReceived(left)}`, 56 | (right) => `Received Right: ` + `${printReceived(right)}`, 57 | (left, right) => 58 | `Received Both:\n` + `Left: ${printReceived(left)}\n` + `Right: ${printReceived(right)}`, 59 | )(received); 60 | } 61 | 62 | /** 63 | * Construct a string that either shows the received Right value or indicates that a Left or Both 64 | * was received. 65 | */ 66 | export function printReceivedRight( 67 | received: EitherOrThese, 68 | addPadding = false, 69 | ): string { 70 | const padding = addPadding ? ' ' : ''; 71 | return fold( 72 | () => `Received a Left.`, 73 | (right) => `Received Right: ` + padding + `${printReceived(right)}`, 74 | () => `Received a Both.`, 75 | )(received); 76 | } 77 | 78 | /** 79 | * Construct a string that shows the difference between a received and expected Left value 80 | */ 81 | export function diffReceivedLeft( 82 | received: EitherOrThese, 83 | expected: unknown, 84 | ): string { 85 | return fold( 86 | (left) => printDiffOrStringify(expected, left, 'Expected Left', 'Received Left', true), 87 | () => `Expected Left: ${printExpected(expected)}\n` + printReceivedValue(received), 88 | () => `Expected Left: ${printExpected(expected)}\n` + printReceivedValue(received), 89 | )(received); 90 | } 91 | 92 | /** 93 | * Construct a string that shows the difference between a received and expected Right value 94 | */ 95 | export function diffReceivedRight( 96 | received: EitherOrThese, 97 | expected: unknown, 98 | ): string { 99 | return fold( 100 | () => `Expected Right: ${printExpected(expected)}\n` + printReceivedValue(received), 101 | (right) => printDiffOrStringify(expected, right, 'Expected Right', 'Received Right', true), 102 | () => `Expected Right: ${printExpected(expected)}\n` + printReceivedValue(received), 103 | )(received); 104 | } 105 | -------------------------------------------------------------------------------- /src/eitherOrTheseMatchers/toSubsetEqualLeft.ts: -------------------------------------------------------------------------------- 1 | import { matcherHint, printExpected, printReceived } from 'jest-matcher-utils'; 2 | import { applyPredicateLeft } from '../eitherOrThese/applyPredicate'; 3 | import { subsetEquals, equals, isEitherOrThese } from '../predicates'; 4 | import { diffReceivedLeft, printReceivedValue } from '../eitherOrThese/print'; 5 | import { EitherOrThese } from '../eitherOrThese/eitherOrThese'; 6 | import { left } from 'fp-ts/lib/These'; 7 | 8 | const passMessage = (received: EitherOrThese, expected: unknown) => () => { 9 | const hint = matcherHint('.not.toSubsetEqualLeft', 'received', 'expectedLeft') + '\n\n'; 10 | const expectedValue = `Expected Left: not ${printExpected(expected)}`; 11 | const receivedValue = equals(left(expected))(received) 12 | ? '' 13 | : `\n${printReceivedValue(received, true)}`; 14 | return hint + expectedValue + receivedValue; 15 | }; 16 | 17 | const failMessage = (received: unknown, expected: unknown) => () => { 18 | return isEitherOrThese(received) 19 | ? matcherHint('.toSubsetEqualLeft', 'received', 'expectedLeft') + 20 | '\n\n' + 21 | diffReceivedLeft(received, expected) 22 | : matcherHint('.toSubsetEqualLeft', 'received', 'expectedLeft') + 23 | '\n\n' + 24 | 'Received value is not an Either or These.\n' + 25 | `Expected Left: ${printExpected(expected)}\n` + 26 | `Received: ${printReceived(received)}`; 27 | }; 28 | 29 | declare global { 30 | namespace jest { 31 | interface Matchers { 32 | /** 33 | * Used to check if the received value is a Left whose value equals the expected value, or 34 | * whose value is an object with a subset of properties that match the expected object. 35 | * 36 | * Objects match if the received object contains all of the properties in the expected object. 37 | * The received object may contain extra properties that are not in the expected object and 38 | * still match. 39 | * 40 | * Note that an empty expected object will match against any received Left whose value is an 41 | * object. 42 | * 43 | * If an array is passed, each element in the expected array is compared to the corresponding 44 | * element in the received array. Both arrays must be the same length, and each comparison 45 | * must succeed in order to pass. 46 | * 47 | * `.toSubsetEqualLeft(value)` is similar to Jest's `.toMatchObject(object)` except it works 48 | * with both objects and basic types. 49 | */ 50 | readonly toSubsetEqualLeft: (expected: unknown) => R; 51 | } 52 | interface Expect { 53 | /** 54 | * Used to check if the received value is a Left whose value equals the expected value, or 55 | * whose value is an object with a subset of properties that match the expected object. 56 | * 57 | * Objects match if the received object contains all of the properties in the expected object. 58 | * The received object may contain extra properties that are not in the expected object and 59 | * still match. 60 | * 61 | * Note that an empty expected object will match against any received Left whose value is an 62 | * object. 63 | * 64 | * If an array is passed, each element in the expected array is compared to the corresponding 65 | * element in the received array. Both arrays must be the same length, and each comparison 66 | * must succeed in order to pass. 67 | * 68 | * `.toSubsetEqualLeft(value)` is similar to Jest's `.toMatchObject(object)` except it works 69 | * with both objects and basic types. 70 | */ 71 | readonly toSubsetEqualLeft: (expected: unknown) => any; 72 | } 73 | } 74 | } 75 | 76 | /** 77 | * Check that the received value is a Left whose value equals the expected value, or whose value is 78 | * an object with a subset of properties that match the expected object. 79 | */ 80 | export const toSubsetEqualLeft = (received: unknown, expected: unknown): any => { 81 | const predicate = subsetEquals(expected); 82 | const pass = isEitherOrThese(received) && applyPredicateLeft(predicate)(received); 83 | 84 | return { 85 | pass: pass, 86 | message: pass ? passMessage(received, expected) : failMessage(received, expected), 87 | }; 88 | }; 89 | -------------------------------------------------------------------------------- /src/theseMatchers/toSubsetEqualBoth.ts: -------------------------------------------------------------------------------- 1 | import { matcherHint, printExpected, printReceived } from 'jest-matcher-utils'; 2 | import { subsetEquals, isThese } from '../predicates'; 3 | import { applyPredicateBoth } from '../these/applyPredicate'; 4 | import { diffReceivedBoth } from '../these/print'; 5 | 6 | const passMessage = (expectedLeft: unknown, expectedRight: unknown) => () => 7 | matcherHint('.not.toSubsetEqualBoth', 'received', 'expectedBoth') + 8 | '\n\n' + 9 | `Expected Both: not \n` + 10 | ` Left: ${printExpected(expectedLeft)}\n` + 11 | ` Right: ${printExpected(expectedRight)}`; 12 | 13 | const failMessage = (received: unknown, expectedLeft: unknown, expectedRight: unknown) => () => { 14 | return isThese(received) 15 | ? matcherHint('.toSubsetEqualBoth', 'received', 'expectedLeft, expectedRight') + 16 | '\n\n' + 17 | diffReceivedBoth(received, expectedLeft, expectedRight, true) 18 | : matcherHint('.toSubsetEqualBoth', 'received', 'expectedLeft, expectedRight') + 19 | '\n\n' + 20 | 'Received value is not a These.\n' + 21 | `Expected Both:\n` + 22 | ` Left: ${printExpected(expectedLeft)}\n` + 23 | ` Right: ${printExpected(expectedRight)}\n` + 24 | `Received: ${printReceived(received)}`; 25 | }; 26 | 27 | declare global { 28 | namespace jest { 29 | interface Matchers { 30 | /** 31 | * Used to check if a value is a Both whose left and right values equal or subset match the 32 | * `expectedLeft` and `expectedRight` values. 33 | * 34 | * A subset match passes when a received value is an object with a subset of properties that 35 | * match the expected object. The received value must contain all of the expected properties, 36 | * and may contain more than the expected properties. 37 | * 38 | * Note that an empty expected object will match against any received object. 39 | * 40 | * If an array is passed as an expected value, each element in the expected array is compared 41 | * to the corresponding element in the received array. Both arrays must be the same length, 42 | * and each comparison must succeed in order to pass. 43 | * 44 | * Note: `toSubsetEqualSome(value)` is similar to Jest's `toMatchObject(object)` except it 45 | * works with both objects and basic types. 46 | */ 47 | readonly toSubsetEqualBoth: (expectedLeft: unknown, expectedRight: unknown) => R; 48 | } 49 | interface Expect { 50 | /** 51 | * Used to check if a value is a Both whose left and right values equal or subset match the 52 | * `expectedLeft` and `expectedRight` values. 53 | * 54 | * A subset match passes when a received value is an object with a subset of properties that 55 | * match the expected object. The received value must contain all of the expected properties, 56 | * and may contain more than the expected properties. 57 | * 58 | * Note that an empty expected object will match against any received object. 59 | * 60 | * If an array is passed as an expected value, each element in the expected array is compared 61 | * to the corresponding element in the received array. Both arrays must be the same length, 62 | * and each comparison must succeed in order to pass. 63 | * 64 | * Note: `toSubsetEqualSome(value)` is similar to Jest's `toMatchObject(object)` except it 65 | * works with both objects and basic types. 66 | */ 67 | readonly toSubsetEqualBoth: (expectedLeft: unknown, expectedRight: unknown) => any; 68 | } 69 | } 70 | } 71 | 72 | /** 73 | * Check if a value is a Both whose left and right values equal or subset match the `expectedLeft` 74 | * `expectedRight` values. 75 | */ 76 | export const toSubsetEqualBoth = ( 77 | received: unknown, 78 | expectedLeft: unknown, 79 | expectedRight: unknown, 80 | ): any => { 81 | const predicateLeft = subsetEquals(expectedLeft); 82 | const predicateRight = subsetEquals(expectedRight); 83 | const pass = isThese(received) && applyPredicateBoth(predicateLeft, predicateRight)(received); 84 | 85 | const message = pass 86 | ? passMessage(expectedLeft, expectedRight) 87 | : failMessage(received, expectedLeft, expectedRight); 88 | 89 | return { 90 | pass, 91 | message, 92 | }; 93 | }; 94 | -------------------------------------------------------------------------------- /src/eitherOrTheseMatchers/toSubsetEqualRight.ts: -------------------------------------------------------------------------------- 1 | import { matcherHint, printExpected, printReceived } from 'jest-matcher-utils'; 2 | import { applyPredicateRight } from '../eitherOrThese/applyPredicate'; 3 | import { equals, subsetEquals, isEitherOrThese } from '../predicates'; 4 | import { diffReceivedRight, printReceivedRight } from '../eitherOrThese/print'; 5 | import { EitherOrThese } from '../eitherOrThese/eitherOrThese'; 6 | import { right } from 'fp-ts/lib/Either'; 7 | 8 | const passMessage = (received: EitherOrThese, expected: unknown) => () => { 9 | const hint = matcherHint('.not.toSubsetEqualRight', 'received', 'expectedRight') + '\n\n'; 10 | const expectedValue = `Expected Right: not ${printExpected(expected)}`; 11 | const receivedValue = equals(right(expected))(received) 12 | ? '' 13 | : `\n${printReceivedRight(received, true)}`; 14 | return hint + expectedValue + receivedValue; 15 | }; 16 | 17 | const failMessage = (received: unknown, expected: unknown) => () => { 18 | return isEitherOrThese(received) 19 | ? matcherHint('.toSubsetEqualRight', 'received', 'expectedRight') + 20 | '\n\n' + 21 | diffReceivedRight(received, expected) 22 | : matcherHint('.toSubsetEqualRight', 'received', 'expectedRight') + 23 | '\n\n' + 24 | 'Received value is not an Either or These.\n' + 25 | `Expected Right: ${printExpected(expected)}\n` + 26 | `Received: ${printReceived(received)}`; 27 | }; 28 | 29 | declare global { 30 | namespace jest { 31 | interface Matchers { 32 | /** 33 | * Used to check if he received value is a Right whose value equals the expected value, or 34 | * whose value is an object with a subset of properties that match the expected object. 35 | * 36 | * Objects match if the received object contains all of the properties in the expected object. 37 | * The received object may contain extra properties that are not in the expected object and 38 | * still match. 39 | * 40 | * Note that an empty expected object will match against any received Right whose value is an 41 | * object. 42 | * 43 | * If an array is passed, each element in the expected array is compared to the corresponding 44 | * element in the received array. Both arrays must be the same length, and each comparison 45 | * must succeed in order to pass. 46 | * 47 | * Note: `.toSubsetEqualRight(value)` is similar to Jest's `.toMatchObject(object)` except it 48 | * works with both objects and basic types. 49 | */ 50 | readonly toSubsetEqualRight: (expected: unknown) => R; 51 | } 52 | interface Expect { 53 | /** 54 | * Used to check if he received value is a Right whose value equals the expected value, or 55 | * whose value is an object with a subset of properties that match the expected object. 56 | * 57 | * Objects match if the received object contains all of the properties in the expected object. 58 | * The received object may contain extra properties that are not in the expected object and 59 | * still match. 60 | * 61 | * Note that an empty expected object will match against any received Right whose value is an 62 | * object. 63 | * 64 | * If an array is passed, each element in the expected array is compared to the corresponding 65 | * element in the received array. Both arrays must be the same length, and each comparison 66 | * must succeed in order to pass. 67 | * 68 | * Note: `.toSubsetEqualRight(value)` is similar to Jest's `.toMatchObject(object)` except it 69 | * works with both objects and basic types. 70 | */ 71 | readonly toSubsetEqualRight: (expected: unknown) => any; 72 | } 73 | } 74 | } 75 | 76 | /** 77 | * Check that the received value is a Right whose value equals the expected value, or whose value is 78 | * an object with a subset of properties that match the expected object. 79 | */ 80 | export const toSubsetEqualRight = (received: unknown, expected: unknown): any => { 81 | const predicate = subsetEquals(expected); 82 | const pass = isEitherOrThese(received) && applyPredicateRight(predicate)(received); 83 | 84 | return { 85 | pass: pass, 86 | message: pass ? passMessage(received, expected) : failMessage(received, expected), 87 | }; 88 | }; 89 | -------------------------------------------------------------------------------- /src/optionMatchers/toSubsetEqualSome.ts: -------------------------------------------------------------------------------- 1 | import { matcherHint, printExpected, printReceived } from 'jest-matcher-utils'; 2 | import { Option, some } from 'fp-ts/lib/Option'; 3 | import { applyPredicate } from '../option/applyPredicate'; 4 | import { isOption, subsetEquals, equals } from '../predicates'; 5 | import { diffReceivedSome, printReceivedOption } from '../option/print'; 6 | 7 | const passMessage = (received: Option, expected: unknown) => () => { 8 | const hint = matcherHint('.not.toSubsetEqualSome', 'received', 'expectedSome') + '\n\n'; 9 | const expectedValue = `Expected Some: not ${printExpected(expected)}`; 10 | const receivedValue = equals(some(expected))(received) 11 | ? '' 12 | : `\n${printReceivedOption(received)}`; 13 | return hint + expectedValue + receivedValue; 14 | }; 15 | 16 | const failMessage = (received: unknown, expected: unknown) => () => { 17 | return isOption(received) 18 | ? matcherHint('.toSubsetEqualSome', 'received', 'expectedSome') + 19 | '\n\n' + 20 | diffReceivedSome(received, expected) 21 | : matcherHint('.toSubsetEqualSome', 'received', 'expectedSome') + 22 | '\n\n' + 23 | 'Received value is not an Option.\n' + 24 | `Expected Some: ${printExpected(expected)}\n` + 25 | `Received: ${printReceived(received)}`; 26 | }; 27 | 28 | declare global { 29 | namespace jest { 30 | interface Matchers { 31 | /** 32 | * Used to check that the received value is a Some whose value equals the expected value, or 33 | * whose value is an object with a subset of properties that match the expected object. 34 | * 35 | * Objects match if the received object contains all of the properties in the expected object. 36 | * The received object may contain extra properties that are not in the expected object and 37 | * still match. 38 | * 39 | * If an array is passed, each element in the expected array is compared to the corresponding 40 | * element in the received array. Both arrays must be the same length, and each comparison 41 | * must succeed in order to pass. 42 | * 43 | * Note: `toSubsetEqualSome(value)` is similar to Jest's `toMatchObject(object)` except it 44 | * works with both objects and basic types. 45 | */ 46 | readonly toSubsetEqualSome: (expected: unknown) => R; 47 | } 48 | interface Expect { 49 | /** 50 | * Used to check that the received value is a Some whose value equals the expected value, or 51 | * whose value is an object with a subset of properties that match the expected object. 52 | * 53 | * Objects match if the received object contains all of the properties in the expected object. 54 | * The received object may contain extra properties that are not in the expected object and 55 | * still match. 56 | * 57 | * If an array is passed, each element in the expected array is compared to the corresponding 58 | * element in the received array. Both arrays must be the same length, and each comparison 59 | * must succeed in order to pass. 60 | * 61 | * Note: `toSubsetEqualSome(value)` is similar to Jest's `toMatchObject(object)` except it 62 | * works with both objects and basic types. 63 | */ 64 | readonly toSubsetEqualSome: (expected: unknown) => any; 65 | } 66 | } 67 | } 68 | 69 | /** 70 | * @summary 71 | * Check that the received value is a Some whose value equals the expected value, or whose value is 72 | * an object with a subset of properties that match the expected object. 73 | * 74 | * @description 75 | * Objects match if the received object contains all of the properties in the expected object. The 76 | * received object may contain extra properties that are not in the expected object and still match. 77 | * 78 | * If an array is passed, each element in the expected array is compared to the corresponding 79 | * element in the received array. Both arrays must be the same length, and each comparison must 80 | * succeed in order to pass. 81 | * 82 | * Note: `toSubsetEqualSome(value)` is similar to Jest's `toMatchObject(object)` except it works 83 | * with both objects and basic types. 84 | */ 85 | export const toSubsetEqualSome = (received: unknown, expected: unknown): any => { 86 | const predicate = subsetEquals(expected); 87 | const pass = isOption(received) && applyPredicate(predicate)(received); 88 | 89 | return { 90 | pass: pass, 91 | message: pass ? passMessage(received, expected) : failMessage(received, expected), 92 | }; 93 | }; 94 | -------------------------------------------------------------------------------- /src/optionMatchers/__tests__/toStrictEqualSome.test.ts: -------------------------------------------------------------------------------- 1 | import { some, none } from 'fp-ts/lib/Option'; 2 | import { toStrictEqualSome } from '../../index'; 3 | 4 | expect.extend({ toStrictEqualSome }); 5 | 6 | class Message { 7 | message: string; 8 | constructor(message: string) { 9 | this.message = message; 10 | } 11 | } 12 | 13 | describe('.toStrictEqualSome should pass', () => { 14 | test('if received is a Some with the expected value', () => { 15 | expect(some('Some')).toStrictEqualSome('Some'); 16 | }); 17 | test('if received is a Some with an undefined expected value', () => { 18 | expect(some(undefined)).toStrictEqualSome(undefined); 19 | }); 20 | test('if the received is a Some with a null expected value', () => { 21 | expect(some(null)).toStrictEqualSome(null); 22 | }); 23 | test('if called as an asymmetric matcher', () => { 24 | expect(some('Any sufficiently advanced technology is equivalent to magic.')).toEqual( 25 | expect.toStrictEqualSome('Any sufficiently advanced technology is equivalent to magic.'), 26 | ); 27 | }); 28 | test('if called with an asymmetric matcher', () => { 29 | expect(some('You affect the world by what you browse.')).toStrictEqualSome( 30 | expect.stringContaining('world'), 31 | ); 32 | }); 33 | }); 34 | 35 | describe('.toStrictEqualSome should fail', () => { 36 | test('if received is a None', () => { 37 | expect(() => expect(none).toStrictEqualSome('Some')).toThrowErrorMatchingInlineSnapshot(` 38 | expect(received).toStrictEqualSome(expectedSome) 39 | 40 | Expected Some: "Some" 41 | Received None 42 | `); 43 | }); 44 | test('if received is a Some that does not equal the expected value', () => { 45 | expect(() => expect(some('another Some')).toStrictEqualSome('Some')) 46 | .toThrowErrorMatchingInlineSnapshot(` 47 | expect(received).toStrictEqualSome(expectedSome) 48 | 49 | Expected Some: "Some" 50 | Received Some: "another Some" 51 | `); 52 | }); 53 | test('if received is a Some that does not strictly equal the expected sparse array', () => { 54 | // eslint-disable-next-line no-sparse-arrays 55 | expect(() => expect(some([1, undefined, 3])).toStrictEqualSome([1, , 3])) 56 | .toThrowErrorMatchingInlineSnapshot(` 57 | expect(received).toStrictEqualSome(expectedSome) 58 | 59 | Expected Some: [1, , 3] 60 | Received Some: [1, undefined, 3] 61 | `); 62 | }); 63 | test('if received is a Some that does not strictly equal the expected class instance', () => { 64 | expect(() => 65 | expect(some({ message: 'Does not compute!' })).toStrictEqualSome( 66 | new Message('Does not compute!'), 67 | ), 68 | ).toThrowErrorMatchingInlineSnapshot(` 69 | expect(received).toStrictEqualSome(expectedSome) 70 | 71 | - Expected Some - 1 72 | + Received Some + 1 73 | 74 | - Message { 75 | + Object { 76 | "message": "Does not compute!", 77 | } 78 | `); 79 | }); 80 | test('if received is a Some with a number value that does not equal the expected value', () => { 81 | expect(() => expect(some(1)).toStrictEqualSome(2)).toThrowErrorMatchingInlineSnapshot(` 82 | expect(received).toStrictEqualSome(expectedSome) 83 | 84 | Expected Some: 2 85 | Received Some: 1 86 | `); 87 | }); 88 | test('if received value is not an Option', () => { 89 | expect(() => expect(undefined).toStrictEqualSome(undefined)) 90 | .toThrowErrorMatchingInlineSnapshot(` 91 | expect(received).toStrictEqualSome(expectedSome) 92 | 93 | Received value is not an Option. 94 | Expected Some: undefined 95 | Received: undefined 96 | `); 97 | }); 98 | }); 99 | 100 | describe('.not.toStrictEqualSome should pass', () => { 101 | test('if received is a Some that does not have the expected value', () => { 102 | expect(some('Some')).not.toStrictEqualSome('A different value'); 103 | }); 104 | test('if received is a None', () => { 105 | expect(none).not.toStrictEqualSome('None'); 106 | }); 107 | test('if received value is not an Option', () => { 108 | expect(1).not.toStrictEqualSome(1); 109 | }); 110 | }); 111 | 112 | describe('.not.toStrictEqualSome should fail', () => { 113 | test('if received is a Some that equals the expected value', () => { 114 | expect(() => expect(some('Some')).not.toStrictEqualSome('Some')) 115 | .toThrowErrorMatchingInlineSnapshot(` 116 | expect(received).not.toStrictEqualSome(expectedSome) 117 | 118 | Expected Some: not "Some" 119 | `); 120 | }); 121 | }); 122 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project 6 | adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [2.1.1] - 2023/06/15 9 | 10 | ### Added 11 | 12 | - README setup instructions for vitest 13 | 14 | ## [2.1.0] - 2023/06/14 15 | 16 | ### Added 17 | 18 | - `.toBeLeftErrorMatching(string | RegExp)` custom matcher. 19 | 20 | ### Fixed 21 | 22 | - `.toBeLeftWithErrorsMatching(Array)` type declaration now correctly specifies 23 | `Array` instead of `unknown` . 24 | 25 | ### Changed 26 | 27 | - [dev] Updated dependencies to their latest versions. 28 | 29 | ## [2.0.2] - 2022/05/05 30 | 31 | ### Fixed 32 | 33 | - Jest 28 support. 34 | 35 | ## [2.0.1] - 2022/04/06 36 | 37 | ### Changed 38 | 39 | - [dev] Updated dependencies to their latest versions. 40 | 41 | ## [2.0.0] - 2022/02/09 42 | 43 | ### Added 44 | 45 | - Custom matchers for values that are wrapped inside an `fp-ts` `These`: 46 | - `.toBeThese()` 47 | - `.toBeBoth()` 48 | - `.toEqualBoth(leftValue, rightValue)` 49 | - `.toStrictEqualBoth(leftValue, rightValue)` 50 | - `.toSubsetEqualBoth(leftValue, rightValue)` 51 | - Intellisense descriptions for all matchers. 52 | 53 | ### Changed 54 | 55 | - **BREAKING** All `Left` and `Right` matchers now officially support both `Either` and `These` 56 | types. Documentation and failure messages have been updated accordingly. This change will 57 | break tests that are expecting specific failure messages. 58 | - **BREAKING** Matcher failure messages have been adjusted to align more closely with the messages 59 | returned by similar matchers in jest. This change will break tests that are expecting specific 60 | failure messages. 61 | - **BREAKING** The types for received values in all matchers are now correctly represented as 62 | `unknown` and type guards are used to confirm that received values match expected types. Matchers 63 | that previously threw errors when bad types were received (values not wrapped in the expected 64 | `fp-ts` container type) now pass when using the `.not` modifier, and fail with better messages 65 | without the `.not` modifier. This change will break tests that are expecting specific failure 66 | messages, and tests using the `.not` modifier that were previously expected to fail due to a type 67 | error (wrong fp-ts container type, or no fp-ts container). 68 | - README updated to show how asymmetric matchers can be used to specify wildcard expected values. 69 | - [dev] Internal matcher unit tests now check matcher failure messages against recorded snapshots to 70 | guard against unintentional breaking interface changes. 71 | - [dev] Updated jest config to use `@relmify/jest-serializer-strip-ansi/always` and 72 | `jest-snapshot-serializer-raw/always` for improved snapshot readability in internal unit tests. 73 | - [dev] Updated dependencies to their latest versions. 74 | 75 | ### Fixed 76 | 77 | - [dev] VS Code Intellisense now works in internal matcher unit tests. 78 | 79 | ## [1.1.1] - 2020/07/21 80 | 81 | ### Changed 82 | 83 | - Updated dependencies to their latest versions. 84 | - README installation instructions now mention that both `fp-ts` and `io-ts` are peer dependencies. 85 | 86 | ## [1.1.0] - 2020/07/13 87 | 88 | ### Added 89 | 90 | - Custom matchers for values that are wrapped inside an `fp-ts` `Option` 91 | 92 | ## [1.0.1] - 2020/07/08 93 | 94 | ### Changed 95 | 96 | - Fix typos in README. 97 | 98 | ## [1.0.0] - 2020/06/10 99 | 100 | ### Added 101 | 102 | - Custom matchers for values that are wrapped inside an `fp-ts` `Either`. 103 | - Custom matcher for the `Left` type returned by `io-ts` when a `decode()` fails. 104 | - A README.md file to describe the purpose of this package and how to use it. 105 | - A CONTRIBUTING.md file to describe how to contribute. 106 | - This CHANGELOG.md file to describe notable changes between releases. 107 | 108 | [2.1.1]: https://github.com/relmify/jest-fp-ts/compare/v2.1.0...v2.1.1 109 | [2.1.0]: https://github.com/relmify/jest-fp-ts/compare/v2.0.2...v2.1.0 110 | [2.0.2]: https://github.com/relmify/jest-fp-ts/compare/v2.0.1...v2.0.2 111 | [2.0.1]: https://github.com/relmify/jest-fp-ts/compare/v2.0.0...v2.0.1 112 | [2.0.0]: https://github.com/relmify/jest-fp-ts/compare/v1.1.1...v2.0.0 113 | [1.1.1]: https://github.com/relmify/jest-fp-ts/compare/v1.1.0...v1.1.1 114 | [1.1.0]: https://github.com/relmify/jest-fp-ts/compare/v1.0.1...v1.1.0 115 | [1.0.1]: https://github.com/relmify/jest-fp-ts/compare/v1.0.0...v1.0.1 116 | [1.0.0]: https://github.com/relmify/jest-fp-ts/releases/tag/v1.0.0 117 | -------------------------------------------------------------------------------- /src/optionMatchers/__tests__/toSubsetEqualSome.test.ts: -------------------------------------------------------------------------------- 1 | import { some, none } from 'fp-ts/lib/Option'; 2 | import { toSubsetEqualSome } from '../../index'; 3 | 4 | expect.extend({ toSubsetEqualSome }); 5 | 6 | describe('.toSubsetEqualSome should pass', () => { 7 | test('if the received is a Some with a subset of properties that match the expected value', () => { 8 | expect(some({ orderId: '123', orderNumber: 100 })).toSubsetEqualSome({ orderNumber: 100 }); 9 | }); 10 | test('if received is a Some with the expected value', () => { 11 | expect(some('Some')).toSubsetEqualSome('Some'); 12 | }); 13 | test('if received is a Some with an undefined expected value', () => { 14 | expect(some(undefined)).toSubsetEqualSome(undefined); 15 | }); 16 | test('if the received is a Some with a null expected value', () => { 17 | expect(some(null)).toSubsetEqualSome(null); 18 | }); 19 | test('if called as an asymmetric matcher', () => { 20 | expect(some('Any sufficiently advanced technology is equivalent to magic.')).toEqual( 21 | expect.toSubsetEqualSome('Any sufficiently advanced technology is equivalent to magic.'), 22 | ); 23 | }); 24 | test('if called with an asymmetric matcher', () => { 25 | expect(some('You affect the world by what you browse.')).toSubsetEqualSome( 26 | expect.stringContaining('world'), 27 | ); 28 | }); 29 | }); 30 | 31 | describe('.toSubsetEqualSome should fail', () => { 32 | test('if the received is a Some that does not contain all of the expected properties', () => { 33 | expect(() => 34 | expect(some({ orderNumber: 100 })).toSubsetEqualSome({ orderId: '123', orderNumber: 100 }), 35 | ).toThrowErrorMatchingInlineSnapshot(` 36 | expect(received).toSubsetEqualSome(expectedSome) 37 | 38 | - Expected Some - 1 39 | + Received Some + 0 40 | 41 | Object { 42 | - "orderId": "123", 43 | "orderNumber": 100, 44 | } 45 | `); 46 | }); 47 | test('if the received is a Some with an array of objects that does not contain all of the expected objects', () => { 48 | expect(() => 49 | expect(some([{ a: 'a' }, { b: 'b' }])).toSubsetEqualSome([ 50 | { a: 'a' }, 51 | { b: 'b' }, 52 | { c: 'c' }, 53 | ]), 54 | ).toThrowErrorMatchingInlineSnapshot(` 55 | expect(received).toSubsetEqualSome(expectedSome) 56 | 57 | - Expected Some - 3 58 | + Received Some + 0 59 | 60 | Array [ 61 | Object { 62 | "a": "a", 63 | }, 64 | Object { 65 | "b": "b", 66 | }, 67 | - Object { 68 | - "c": "c", 69 | - }, 70 | ] 71 | `); 72 | }); 73 | test('if the received is a Some with an array of objects that contains more than the expected objects', () => { 74 | expect(() => 75 | expect(some([{ a: 'a' }, { b: 'b' }, { c: 'c' }])).toSubsetEqualSome([ 76 | { a: 'a' }, 77 | { b: 'b' }, 78 | ]), 79 | ).toThrowErrorMatchingInlineSnapshot(` 80 | expect(received).toSubsetEqualSome(expectedSome) 81 | 82 | - Expected Some - 0 83 | + Received Some + 3 84 | 85 | Array [ 86 | Object { 87 | "a": "a", 88 | }, 89 | Object { 90 | "b": "b", 91 | }, 92 | + Object { 93 | + "c": "c", 94 | + }, 95 | ] 96 | `); 97 | }); 98 | test('if received is a None', () => { 99 | expect(() => expect(none).toSubsetEqualSome('Some')).toThrowErrorMatchingInlineSnapshot(` 100 | expect(received).toSubsetEqualSome(expectedSome) 101 | 102 | Expected Some: "Some" 103 | Received None 104 | `); 105 | }); 106 | test('if received is a Some that does not equal the expected value', () => { 107 | expect(() => expect(some('another Some')).toSubsetEqualSome('Some')) 108 | .toThrowErrorMatchingInlineSnapshot(` 109 | expect(received).toSubsetEqualSome(expectedSome) 110 | 111 | Expected Some: "Some" 112 | Received Some: "another Some" 113 | `); 114 | }); 115 | test('if received is a Some with a number value that does not equal the expected value', () => { 116 | expect(() => expect(some(1)).toSubsetEqualSome(2)).toThrowErrorMatchingInlineSnapshot(` 117 | expect(received).toSubsetEqualSome(expectedSome) 118 | 119 | Expected Some: 2 120 | Received Some: 1 121 | `); 122 | }); 123 | test('if received is not an Option value', () => { 124 | expect(() => expect(1).toSubsetEqualSome(expect.anything())) 125 | .toThrowErrorMatchingInlineSnapshot(` 126 | expect(received).toSubsetEqualSome(expectedSome) 127 | 128 | Received value is not an Option. 129 | Expected Some: Anything 130 | Received: 1 131 | `); 132 | }); 133 | }); 134 | 135 | describe('.not.toSubsetEqualSome should pass', () => { 136 | test('if received is a Some that does not have the expected value', () => { 137 | expect(some('Some')).not.toSubsetEqualSome('A different value'); 138 | }); 139 | test('if received is a None', () => { 140 | expect(none).not.toSubsetEqualSome('None'); 141 | }); 142 | test('if received is not an Option value', () => { 143 | expect(1).not.toSubsetEqualSome(expect.any(Number)); 144 | }); 145 | }); 146 | 147 | describe('.not.toSubsetEqualSome should fail', () => { 148 | test('if received is a Some that equals the expected value', () => { 149 | expect(() => expect(some('Some')).not.toSubsetEqualSome('Some')) 150 | .toThrowErrorMatchingInlineSnapshot(` 151 | expect(received).not.toSubsetEqualSome(expectedSome) 152 | 153 | Expected Some: not "Some" 154 | `); 155 | }); 156 | }); 157 | -------------------------------------------------------------------------------- /src/eitherOrTheseMatchers/__tests__/toEqualLeft.test.ts: -------------------------------------------------------------------------------- 1 | import { left as leftEither, right as rightEither } from 'fp-ts/lib/Either'; 2 | import { left as leftThese, right as rightThese, both } from 'fp-ts/lib/These'; 3 | import { some, none } from 'fp-ts/lib/Option'; 4 | import { toEqualLeft } from '../../index'; 5 | 6 | expect.extend({ toEqualLeft }); 7 | 8 | class Message { 9 | message: string; 10 | constructor(message: string) { 11 | this.message = message; 12 | } 13 | } 14 | 15 | describe('.toEqualLeft should pass', () => { 16 | test('if the received is a Left Either with the expected value', () => { 17 | expect(leftEither('Left')).toEqualLeft('Left'); 18 | }); 19 | test('if the received is a Left These with the expected value', () => { 20 | expect(leftThese('Left')).toEqualLeft('Left'); 21 | }); 22 | test('if received is a Left with an undefined expected value', () => { 23 | expect(leftEither(undefined)).toEqualLeft(undefined); 24 | }); 25 | test('if called as an asymmetric matcher', () => { 26 | expect(leftEither('Any sufficiently advanced technology is equivalent to magic.')).toEqual( 27 | expect.toEqualLeft('Any sufficiently advanced technology is equivalent to magic.'), 28 | ); 29 | }); 30 | test('if called with an asymmetric matcher', () => { 31 | expect(leftThese('You affect the world by what you browse.')).toEqualLeft( 32 | expect.stringContaining('world'), 33 | ); 34 | }); 35 | test('if received is a Left that does not strictly equal the expected sparse array', () => { 36 | // eslint-disable-next-line no-sparse-arrays 37 | expect(() => expect(leftThese([1, undefined, 3])).toEqualLeft([1, , 3])); 38 | }); 39 | test('if received is a Left that does not strictly equal the expected class instance', () => { 40 | expect(() => 41 | expect(leftEither({ message: 'Does not compute!' })).toEqualLeft( 42 | new Message('Does not compute!'), 43 | ), 44 | ); 45 | }); 46 | }); 47 | 48 | describe('.toEqualLeft should fail', () => { 49 | test('if received is a Left that does not equal the expected value', () => { 50 | expect(() => expect(leftThese({ a: 1 })).toEqualLeft({ a: 2 })) 51 | .toThrowErrorMatchingInlineSnapshot(` 52 | expect(received).toEqualLeft(expectedLeft) 53 | 54 | - Expected Left - 1 55 | + Received Left + 1 56 | 57 | Object { 58 | - "a": 2, 59 | + "a": 1, 60 | } 61 | `); 62 | }); 63 | test('if received is a Right', () => { 64 | expect(() => expect(rightEither('received right')).toEqualLeft('expected left')) 65 | .toThrowErrorMatchingInlineSnapshot(` 66 | expect(received).toEqualLeft(expectedLeft) 67 | 68 | Expected Left: "expected left" 69 | Received Right: "received right" 70 | `); 71 | }); 72 | test('if received is a Both', () => { 73 | expect(() => expect(both('received left', 'received right')).toEqualLeft('expected left')) 74 | .toThrowErrorMatchingInlineSnapshot(` 75 | expect(received).toEqualLeft(expectedLeft) 76 | 77 | Expected Left: "expected left" 78 | Received Both: 79 | Left: "received left" 80 | Right: "received right" 81 | `); 82 | }); 83 | }); 84 | 85 | describe('.toEqualLeft should fail and indicate not an Either or These', () => { 86 | test('if received is undefined', () => { 87 | expect(() => expect(undefined).toEqualLeft('expected left')) 88 | .toThrowErrorMatchingInlineSnapshot(` 89 | expect(received).toEqualLeft(expectedLeft) 90 | 91 | Received value is not an Either or These. 92 | Expected Left: "expected left" 93 | Received: undefined 94 | `); 95 | }); 96 | test('if received is a Some', () => { 97 | expect(() => expect(some('my some')).toEqualLeft('expected left')) 98 | .toThrowErrorMatchingInlineSnapshot(` 99 | expect(received).toEqualLeft(expectedLeft) 100 | 101 | Received value is not an Either or These. 102 | Expected Left: "expected left" 103 | Received: {"_tag": "Some", "value": "my some"} 104 | `); 105 | }); 106 | test('if received is a basic type', () => { 107 | expect(() => expect(200).toEqualLeft(200)).toThrowErrorMatchingInlineSnapshot(` 108 | expect(received).toEqualLeft(expectedLeft) 109 | 110 | Received value is not an Either or These. 111 | Expected Left: 200 112 | Received: 200 113 | `); 114 | }); 115 | }); 116 | 117 | describe('.not.toEqualLeft should pass', () => { 118 | test('if received is a Left that does not equal the expected value', () => { 119 | expect(leftEither('Left')).not.toEqualLeft('A different value'); 120 | }); 121 | test('if received is a Right', () => { 122 | expect(rightThese('Hello')).not.toEqualLeft('Hello'); 123 | }); 124 | test('if received is a Both', () => { 125 | expect(both('Right', 'Right')).not.toEqualLeft('Right'); 126 | }); 127 | test('if received is null', () => { 128 | expect(null).not.toEqualLeft(null); 129 | }); 130 | test('if received is a None', () => { 131 | expect(none).not.toEqualLeft(none); 132 | }); 133 | }); 134 | 135 | describe('.not.toEqualLeft should fail', () => { 136 | test('if received is a Left Either that equals the expected value', () => { 137 | expect(() => expect(leftEither('left')).not.toEqualLeft('left')) 138 | .toThrowErrorMatchingInlineSnapshot(` 139 | expect(received).not.toEqualLeft(expectedLeft) 140 | 141 | Expected Left: not "left" 142 | `); 143 | }); 144 | test('if received is a Left These that equals the expected value', () => { 145 | expect(() => expect(leftThese('left')).not.toEqualLeft('left')) 146 | .toThrowErrorMatchingInlineSnapshot(` 147 | expect(received).not.toEqualLeft(expectedLeft) 148 | 149 | Expected Left: not "left" 150 | `); 151 | }); 152 | }); 153 | -------------------------------------------------------------------------------- /src/predicates/__tests__/equals.test.ts: -------------------------------------------------------------------------------- 1 | import { equals, strictEquals, subsetEquals } from '../index'; 2 | 3 | /* eslint-disable @typescript-eslint/no-unused-vars */ 4 | 5 | class Message { 6 | message: string; 7 | constructor(message: string) { 8 | this.message = message; 9 | } 10 | } 11 | class CustomError extends Error { 12 | reason: string | undefined; 13 | constructor(message: string, reason?: string) { 14 | super(message); 15 | Object.defineProperty(this, 'name', { value: 'CustomError' }); 16 | Object.defineProperty(this, 'reason', { value: reason }); 17 | } 18 | } 19 | 20 | const trueForAllData: Array> = [ 21 | // [message, received, expected] 22 | ['equal strings', 'hello!', 'hello!'], 23 | ['equal numbers', 1, 1], 24 | ['equal arrays', ['a', 'b'], ['a', 'b']], 25 | ['equal objects', { orderId: '123' }, { orderId: '123' }], 26 | [ 27 | 'equal deep objects', 28 | { orderId: '123', orderLines: { item: 'soup' } }, 29 | { orderId: '123', orderLines: { item: 'soup' } }, 30 | ], 31 | ['equivalent objects', new Message('Does not compute!'), new Message('Does not compute!')], 32 | ['undefined values', undefined, undefined], 33 | ['null values', null, null], 34 | ]; 35 | 36 | const falseForAllData: Array> = [ 37 | // [message, received, expected] 38 | ['non equal strings', 'hello!', 'hi'], 39 | ['non equal strings where the expected string is empty', 'hello!', ''], 40 | ['non equal strings where the expected string is undefined', 'hello!', undefined], 41 | ['non equal strings where the expected string is null', 'hello!', null], 42 | ['non equal numbers', 1, 2], 43 | ['non equal arrays with the same number of elements', ['a', 'b'], ['a', 'z']], 44 | ['non equal boolean values', true, false], 45 | ['null versus undefined', null, undefined], 46 | ['objects with the same keys but different values', { orderId: '123' }, { orderId: '456' }], 47 | ['objects with different keys but the same values', { orderId: '123' }, { orderNumber: '123' }], 48 | [ 49 | 'non equal deep objects', 50 | { orderId: '123', orderItem: { item: 'soup' } }, 51 | { orderId: '123', orderItem: { item: 'crackers' } }, 52 | ], 53 | [ 54 | 'arrays of objects where the expected array contains extra objects', 55 | [{ a: 'a' }, { b: 'b' }], 56 | [{ a: 'a' }, { b: 'b' }, { c: 'c' }], 57 | ], 58 | [ 59 | 'arrays of objects where the received array contains extra objects', 60 | [{ a: 'a' }, { b: 'b' }, { c: 'c' }], 61 | [{ a: 'a' }, { b: 'b' }], 62 | ], 63 | [ 64 | 'objects where the expected object contains an additional property', 65 | { orderId: '123' }, 66 | { orderId: '123', orderNumber: 100 }, 67 | ], 68 | ]; 69 | 70 | const falseForStrictEqualsTrueForOthersData: Array> = [ 71 | // [message, received, expected] 72 | ['errors with equal messages but different names', new Error('oops'), new CustomError('oops')], 73 | [ 74 | 'errors with equal messages but different names and number of properties', 75 | new Error('oops'), 76 | new CustomError('oops', 'because'), 77 | ], 78 | // eslint-disable-next-line no-sparse-arrays 79 | ['sparse arrays compared to similar arrays with undefined members', [1, , 3], [1, undefined, 3]], 80 | [ 81 | 'objects where one of the objects contains an extra undefined value', 82 | { orderId: '123', item: 'soup', flavor: undefined }, 83 | { orderId: '123', item: 'soup' }, 84 | ], 85 | [ 86 | 'class instances with the same fields as literal objects', 87 | new Message('Does not compute!'), 88 | { message: 'Does not compute!' }, 89 | ], 90 | ]; 91 | 92 | const trueForSubsetEqualsFalseForOthersData: Array> = [ 93 | // [message, received, expected] 94 | [ 95 | 'objects where the received object contains an additional property', 96 | { orderId: '123', orderNumber: 100 }, 97 | { orderId: '123' }, 98 | ], 99 | ]; 100 | 101 | describe('equals()', () => { 102 | test.each(trueForAllData)('is true for %s', (description, received, expected) => { 103 | expect(equals(expected)(received)).toBe(true); 104 | }); 105 | test.each(falseForAllData)('is false for %s', (description, received, expected) => { 106 | expect(equals(expected)(received)).toBe(false); 107 | }); 108 | test.each(falseForStrictEqualsTrueForOthersData)( 109 | 'is true for %s', 110 | (_description, received, expected) => { 111 | expect(equals(expected)(received)).toBe(true); 112 | }, 113 | ); 114 | test.each(trueForSubsetEqualsFalseForOthersData)( 115 | 'is false for %s', 116 | (_description, received, expected) => { 117 | expect(equals(expected)(received)).toBe(false); 118 | }, 119 | ); 120 | }); 121 | 122 | describe('strictEquals()', () => { 123 | test.each(trueForAllData)('is true for %s', (_description, received, expected) => { 124 | expect(strictEquals(expected)(received)).toBe(true); 125 | }); 126 | test.each(falseForAllData)('is false for %s', (_description, received, expected) => { 127 | expect(strictEquals(expected)(received)).toBe(false); 128 | }); 129 | test.each(falseForStrictEqualsTrueForOthersData)( 130 | 'is false for %s', 131 | (_description, received, expected) => { 132 | expect(strictEquals(expected)(received)).toBe(false); 133 | }, 134 | ); 135 | test.each(trueForSubsetEqualsFalseForOthersData)( 136 | 'is false for %s', 137 | (_description, received, expected) => { 138 | expect(strictEquals(expected)(received)).toBe(false); 139 | }, 140 | ); 141 | }); 142 | 143 | describe('subsetEquals()', () => { 144 | test.each(trueForAllData)('is true for %s', (_description, received, expected) => { 145 | expect(subsetEquals(expected)(received)).toBe(true); 146 | }); 147 | test.each(falseForAllData)('is false for %s', (_description, received, expected) => { 148 | expect(subsetEquals(expected)(received)).toBe(false); 149 | }); 150 | test.each(falseForStrictEqualsTrueForOthersData)( 151 | 'is true for %s', 152 | (_description, received, expected) => { 153 | expect(subsetEquals(expected)(received)).toBe(true); 154 | }, 155 | ); 156 | test.each(trueForSubsetEqualsFalseForOthersData)( 157 | 'is true for %s', 158 | (_description, received, expected) => { 159 | expect(subsetEquals(expected)(received)).toBe(true); 160 | }, 161 | ); 162 | }); 163 | -------------------------------------------------------------------------------- /src/theseMatchers/__tests__/toEqualBoth.test.ts: -------------------------------------------------------------------------------- 1 | import { left, right, both } from 'fp-ts/lib/These'; 2 | import { some, none } from 'fp-ts/lib/Option'; 3 | import { toEqualBoth } from '../../index'; 4 | 5 | expect.extend({ toEqualBoth }); 6 | 7 | class Message { 8 | message: string; 9 | constructor(message: string) { 10 | this.message = message; 11 | } 12 | } 13 | 14 | describe('.toEqualBoth should pass', () => { 15 | test('if the received is a Both with the expected value', () => { 16 | expect(both('Left', 'Right')).toEqualBoth('Left', 'Right'); 17 | }); 18 | test('if received is a Both with undefined expected values', () => { 19 | expect(both(undefined, undefined)).toEqualBoth(undefined, undefined); 20 | }); 21 | test('if called as an asymmetric matcher', () => { 22 | expect( 23 | both('Any sufficiently advanced technology is equivalent to magic.', 'Arthur C. Clarke'), 24 | ).toEqual( 25 | expect.toEqualBoth( 26 | 'Any sufficiently advanced technology is equivalent to magic.', 27 | 'Arthur C. Clarke', 28 | ), 29 | ); 30 | }); 31 | test('if called with an asymmetric matcher', () => { 32 | expect(both('You affect the world by what you browse.', 'Tim Berners-Lee')).toEqualBoth( 33 | expect.stringContaining('world'), 34 | expect.stringContaining('Tim'), 35 | ); 36 | }); 37 | test('if received is a Both that does not strictly equal the expected sparse array', () => { 38 | // eslint-disable-next-line no-sparse-arrays 39 | expect(() => expect(both([1, undefined, 3], [undefined, 2])).toEqualBoth([1, , 3], [, 2])); 40 | }); 41 | test('if received is a Both whose values do not strictly equal the expected class instances', () => { 42 | expect(() => 43 | expect(both({ message: 'Does not compute!' }, { message: 'All clear' })).toEqualBoth( 44 | new Message('Does not compute!'), 45 | new Message('All clear'), 46 | ), 47 | ); 48 | }); 49 | }); 50 | 51 | describe('.toEqualBoth should fail', () => { 52 | test('if received is a Both that does not equal the expected value', () => { 53 | expect(() => expect(both({ a: 1 }, { b: 2 })).toEqualBoth({ a: 2 }, { b: 2 })) 54 | .toThrowErrorMatchingInlineSnapshot(` 55 | expect(received).toEqualBoth(expectedLeft, expectedRight) 56 | 57 | Expected Both: 58 | Left: {"a": 2} 59 | Right: {"b": 2} 60 | 61 | Difference from Left: 62 | - Expected - 1 63 | + Received + 1 64 | 65 | Object { 66 | - "a": 2, 67 | + "a": 1, 68 | } 69 | `); 70 | }); 71 | test('if received is a Both that does not match the expected asymmetric matcher', () => { 72 | expect(() => 73 | expect(both({ a: 1 }, { b: 2 })).toEqualBoth(expect.any(Object), expect.any(String)), 74 | ).toThrowErrorMatchingInlineSnapshot(` 75 | expect(received).toEqualBoth(expectedLeft, expectedRight) 76 | 77 | Expected Both: 78 | Left: Any 79 | Right: Any 80 | 81 | Difference from Right: 82 | Expected: Any 83 | Received: {"b": 2} 84 | `); 85 | }); 86 | test('if received is a Left', () => { 87 | expect(() => expect(left('received left')).toEqualBoth('expected left', 'expected right')) 88 | .toThrowErrorMatchingInlineSnapshot(` 89 | expect(received).toEqualBoth(expectedLeft, expectedRight) 90 | 91 | Expected Both: 92 | Left: "expected left" 93 | Right: "expected right" 94 | 95 | Received Left: "received left" 96 | `); 97 | }); 98 | test('if received is a Right', () => { 99 | expect(() => expect(right('received right')).toEqualBoth('expected left', 'expected right')) 100 | .toThrowErrorMatchingInlineSnapshot(` 101 | expect(received).toEqualBoth(expectedLeft, expectedRight) 102 | 103 | Expected Both: 104 | Left: "expected left" 105 | Right: "expected right" 106 | 107 | Received Right: "received right" 108 | `); 109 | }); 110 | }); 111 | 112 | describe('.toEqualBoth should fail and indicate not a These', () => { 113 | test('if received is a Some', () => { 114 | expect(() => expect(some(1)).toEqualBoth(1, 1)).toThrowErrorMatchingInlineSnapshot(` 115 | expect(received).toEqualBoth(expectedLeft, expectedRight) 116 | 117 | Received value is not a These. 118 | Expected Both: 119 | Left: 1 120 | Right: 1 121 | Received: {"_tag": "Some", "value": 1} 122 | `); 123 | }); 124 | test('if received is undefined', () => { 125 | expect(() => expect(undefined).toEqualBoth(1, 2)).toThrowErrorMatchingInlineSnapshot(` 126 | expect(received).toEqualBoth(expectedLeft, expectedRight) 127 | 128 | Received value is not a These. 129 | Expected Both: 130 | Left: 1 131 | Right: 2 132 | Received: undefined 133 | `); 134 | }); 135 | test('if received is a basic type', () => { 136 | expect(() => expect(200).toEqualBoth(404, 200)).toThrowErrorMatchingInlineSnapshot(` 137 | expect(received).toEqualBoth(expectedLeft, expectedRight) 138 | 139 | Received value is not a These. 140 | Expected Both: 141 | Left: 404 142 | Right: 200 143 | Received: 200 144 | `); 145 | }); 146 | }); 147 | 148 | describe('.not.toEqualBoth should pass', () => { 149 | test('if received is a Both that does not equal the expected value', () => { 150 | expect(both('Left', 'Right')).not.toEqualBoth('A different left', 'A different right'); 151 | }); 152 | test('if received is a Left', () => { 153 | expect(left('Left')).not.toEqualBoth('Left', 'Left'); 154 | }); 155 | test('if received is a Right', () => { 156 | expect(right('Hello')).not.toEqualBoth('Hello', 'again'); 157 | }); 158 | test('if received is null', () => { 159 | expect(null).not.toEqualBoth(null, null); 160 | }); 161 | test('if received is a None', () => { 162 | expect(none).not.toEqualBoth(none, none); 163 | }); 164 | }); 165 | 166 | describe('.not.toEqualLeft should fail', () => { 167 | test('if received is a Both that equals the expected value', () => { 168 | expect(() => expect(both('a', 'b')).not.toEqualBoth('a', 'b')) 169 | .toThrowErrorMatchingInlineSnapshot(` 170 | expect(received).not.toEqualBoth(expectedBoth) 171 | 172 | Expected Both: not 173 | Left: "a" Right: "b" 174 | `); 175 | }); 176 | }); 177 | -------------------------------------------------------------------------------- /src/eitherOrTheseMatchers/__tests__/toEqualRight.test.ts: -------------------------------------------------------------------------------- 1 | import { left as leftEither, right as rightEither } from 'fp-ts/lib/Either'; 2 | import { left as leftThese, right as rightThese, both } from 'fp-ts/lib/These'; 3 | import { some, none } from 'fp-ts/lib/Option'; 4 | import { toEqualRight } from '../../index'; 5 | 6 | expect.extend({ toEqualRight }); 7 | 8 | class Message { 9 | message: string; 10 | constructor(message: string) { 11 | this.message = message; 12 | } 13 | } 14 | 15 | describe('.toEqualRight should pass', () => { 16 | test('if the received is a Right Either with the expected value', () => { 17 | expect(rightEither('Right')).toEqualRight('Right'); 18 | }); 19 | test('if the received is a Right These with the expected value', () => { 20 | expect(rightThese('Right')).toEqualRight('Right'); 21 | }); 22 | test('if received is a Right with a null expected value', () => { 23 | expect(rightThese(null)).toEqualRight(null); 24 | }); 25 | test('if called as an asymmetric matcher', () => { 26 | expect(rightEither('Any sufficiently advanced technology is equivalent to magic.')).toEqual( 27 | expect.toEqualRight('Any sufficiently advanced technology is equivalent to magic.'), 28 | ); 29 | }); 30 | test('if called with an asymmetric matcher', () => { 31 | expect(rightThese('You affect the world by what you browse.')).toEqualRight( 32 | expect.stringContaining('world'), 33 | ); 34 | }); 35 | test('if received is a Right that does not strictly equal the expected sparse array', () => { 36 | // eslint-disable-next-line no-sparse-arrays 37 | expect(() => expect(rightThese([1, undefined, 3])).toEqualRight([1, , 3])); 38 | }); 39 | test('if received is a Right that does not strictly equal the expected class instance', () => { 40 | expect(() => 41 | expect(rightEither({ message: 'Does not compute!' })).toEqualRight( 42 | new Message('Does not compute!'), 43 | ), 44 | ); 45 | }); 46 | }); 47 | 48 | describe('.toEqualRight should fail and include the expected and received values', () => { 49 | test('if received is a Right that does not equal the expected value', () => { 50 | expect(() => expect(rightThese('received right')).toEqualRight('expected right')) 51 | .toThrowErrorMatchingInlineSnapshot(` 52 | expect(received).toEqualRight(expectedRight) 53 | 54 | Expected Right: "expected right" 55 | Received Right: "received right" 56 | `); 57 | }); 58 | test('if received is a Left', () => { 59 | expect(() => expect(leftEither('received left')).toEqualRight('expected right')) 60 | .toThrowErrorMatchingInlineSnapshot(` 61 | expect(received).toEqualRight(expectedRight) 62 | 63 | Expected Right: "expected right" 64 | Received Left: "received left" 65 | `); 66 | }); 67 | test('if received is a Both', () => { 68 | expect(() => expect(both('received left', 'received right')).toEqualRight('expected right')) 69 | .toThrowErrorMatchingInlineSnapshot(` 70 | expect(received).toEqualRight(expectedRight) 71 | 72 | Expected Right: "expected right" 73 | Received Both: 74 | Left: "received left" 75 | Right: "received right" 76 | `); 77 | }); 78 | }); 79 | 80 | describe('.toEqualRight should fail and indicate not an Either or These', () => { 81 | test('if received is a Some', () => { 82 | expect(() => expect(some('my some')).toEqualRight('expected right')) 83 | .toThrowErrorMatchingInlineSnapshot(` 84 | expect(received).toEqualRight(expectedRight) 85 | 86 | Received value is not an Either or These. 87 | Expected Right: "expected right" 88 | Received: {"_tag": "Some", "value": "my some"} 89 | `); 90 | }); 91 | test('if received is a None', () => { 92 | expect(() => expect(none).toEqualRight(none)).toThrowErrorMatchingInlineSnapshot(` 93 | expect(received).toEqualRight(expectedRight) 94 | 95 | Received value is not an Either or These. 96 | Expected Right: {"_tag": "None"} 97 | Received: {"_tag": "None"} 98 | `); 99 | }); 100 | test('if received is a basic type', () => { 101 | expect(() => expect(200).toEqualRight(200)).toThrowErrorMatchingInlineSnapshot(` 102 | expect(received).toEqualRight(expectedRight) 103 | 104 | Received value is not an Either or These. 105 | Expected Right: 200 106 | Received: 200 107 | `); 108 | }); 109 | test('if received is null', () => { 110 | expect(() => expect(null).toEqualRight(null)).toThrowErrorMatchingInlineSnapshot(` 111 | expect(received).toEqualRight(expectedRight) 112 | 113 | Received value is not an Either or These. 114 | Expected Right: null 115 | Received: null 116 | `); 117 | }); 118 | test('if received is undefined', () => { 119 | expect(() => expect(undefined).toEqualRight('expected right')) 120 | .toThrowErrorMatchingInlineSnapshot(` 121 | expect(received).toEqualRight(expectedRight) 122 | 123 | Received value is not an Either or These. 124 | Expected Right: "expected right" 125 | Received: undefined 126 | `); 127 | }); 128 | }); 129 | 130 | describe('.not.toEqualRight should pass', () => { 131 | test('if received is a Right that does not equal the expected value', () => { 132 | expect(rightEither('Right')).not.toEqualRight('A different value'); 133 | }); 134 | test('if received is a Left', () => { 135 | expect(leftThese('Hello')).not.toEqualRight('Hello'); 136 | }); 137 | test('if received is a Both', () => { 138 | expect(both('Left', 'Left')).not.toEqualRight('Left'); 139 | }); 140 | test('if received is undefined', () => { 141 | expect(undefined).not.toEqualRight(null); 142 | }); 143 | test('if received is a Some', () => { 144 | expect(some(1)).not.toEqualRight(1); 145 | }); 146 | }); 147 | 148 | describe('.not.toEqualRight should fail', () => { 149 | test('if received is a Right Either that equals the expected value', () => { 150 | expect(() => expect(rightEither('right')).not.toEqualRight('right')) 151 | .toThrowErrorMatchingInlineSnapshot(` 152 | expect(received).not.toEqualRight(expectedRight) 153 | 154 | Expected Right: not "right" 155 | `); 156 | }); 157 | test('if received is a Right These that equals the expected value', () => { 158 | expect(() => expect(rightThese('right')).not.toEqualRight('right')) 159 | .toThrowErrorMatchingInlineSnapshot(` 160 | expect(received).not.toEqualRight(expectedRight) 161 | 162 | Expected Right: not "right" 163 | `); 164 | }); 165 | }); 166 | -------------------------------------------------------------------------------- /src/theseMatchers/__tests__/toStrictEqualBoth.test.ts: -------------------------------------------------------------------------------- 1 | import { left, right, both } from 'fp-ts/lib/These'; 2 | import { some, none } from 'fp-ts/lib/Option'; 3 | import { toStrictEqualBoth } from '../../index'; 4 | 5 | expect.extend({ toStrictEqualBoth }); 6 | 7 | class Message { 8 | message: string; 9 | constructor(message: string) { 10 | this.message = message; 11 | } 12 | } 13 | 14 | describe('.toStrictEqualBoth should pass', () => { 15 | test('if the received is a Both with the expected value', () => { 16 | expect(both('Left', 'Right')).toStrictEqualBoth('Left', 'Right'); 17 | }); 18 | test('if received is a Both with undefined expected values', () => { 19 | expect(both(undefined, undefined)).toStrictEqualBoth(undefined, undefined); 20 | }); 21 | test('if called as an asymmetric matcher', () => { 22 | expect( 23 | both('Any sufficiently advanced technology is equivalent to magic.', 'Arthur C. Clarke'), 24 | ).toEqual( 25 | expect.toStrictEqualBoth( 26 | 'Any sufficiently advanced technology is equivalent to magic.', 27 | 'Arthur C. Clarke', 28 | ), 29 | ); 30 | }); 31 | test('if called with an asymmetric matcher', () => { 32 | expect(both('You affect the world by what you browse.', 'Tim Berners-Lee')).toStrictEqualBoth( 33 | expect.stringContaining('world'), 34 | expect.any(String), 35 | ); 36 | }); 37 | }); 38 | 39 | describe('.toStrictEqualBoth should fail', () => { 40 | test('if received is a Both that does not equal the expected value', () => { 41 | expect(() => expect(both({ a: 1 }, { b: 2 })).toStrictEqualBoth({ a: 2 }, { b: 2 })) 42 | .toThrowErrorMatchingInlineSnapshot(` 43 | expect(received).toStrictEqualBoth(expectedLeft, expectedRight) 44 | 45 | Expected Both: 46 | Left: {"a": 2} 47 | Right: {"b": 2} 48 | 49 | Difference from Left: 50 | - Expected - 1 51 | + Received + 1 52 | 53 | Object { 54 | - "a": 2, 55 | + "a": 1, 56 | } 57 | `); 58 | }); 59 | test('if received is a Left', () => { 60 | expect(() => expect(left('received left')).toStrictEqualBoth('expected left', 'expected right')) 61 | .toThrowErrorMatchingInlineSnapshot(` 62 | expect(received).toStrictEqualBoth(expectedLeft, expectedRight) 63 | 64 | Expected Both: 65 | Left: "expected left" 66 | Right: "expected right" 67 | 68 | Received Left: "received left" 69 | `); 70 | }); 71 | test('if received is a Right', () => { 72 | expect(() => 73 | expect(right('received right')).toStrictEqualBoth('expected left', 'expected right'), 74 | ).toThrowErrorMatchingInlineSnapshot(` 75 | expect(received).toStrictEqualBoth(expectedLeft, expectedRight) 76 | 77 | Expected Both: 78 | Left: "expected left" 79 | Right: "expected right" 80 | 81 | Received Right: "received right" 82 | `); 83 | }); 84 | test('if received is a Both that does not strictly equal the expected sparse array', () => { 85 | expect(() => 86 | // eslint-disable-next-line no-sparse-arrays 87 | expect(both([1, undefined, 3], [undefined, 2])).toStrictEqualBoth([1, , 3], [, 2]), 88 | ).toThrowErrorMatchingInlineSnapshot(` 89 | expect(received).toStrictEqualBoth(expectedLeft, expectedRight) 90 | 91 | Expected Both: 92 | Left: [1, , 3] 93 | Right: [, 2] 94 | 95 | Difference from Left: 96 | Expected: [1, , 3] 97 | Received: [1, undefined, 3] 98 | 99 | Difference from Right: 100 | Expected: [, 2] 101 | Received: [undefined, 2] 102 | `); 103 | }); 104 | test('if received is a Both whose values do not strictly equal the expected class instances', () => { 105 | expect(() => 106 | expect(both({ message: 'Does not compute!' }, { message: 'All clear' })).toStrictEqualBoth( 107 | new Message('Does not compute!'), 108 | new Message('All clear'), 109 | ), 110 | ).toThrowErrorMatchingInlineSnapshot(` 111 | expect(received).toStrictEqualBoth(expectedLeft, expectedRight) 112 | 113 | Expected Both: 114 | Left: {"message": "Does not compute!"} 115 | Right: {"message": "All clear"} 116 | 117 | Difference from Left: 118 | - Expected - 1 119 | + Received + 1 120 | 121 | - Message { 122 | + Object { 123 | "message": "Does not compute!", 124 | } 125 | 126 | Difference from Right: 127 | - Expected - 1 128 | + Received + 1 129 | 130 | - Message { 131 | + Object { 132 | "message": "All clear", 133 | } 134 | `); 135 | }); 136 | }); 137 | 138 | describe('.toStrictEqualBoth should fail and indicate not a These', () => { 139 | test('if received is undefined', () => { 140 | expect(() => expect(undefined).toStrictEqualBoth(1, 2)).toThrowErrorMatchingInlineSnapshot(` 141 | expect(received).toStrictEqualBoth(expectedLeft, expectedRight) 142 | 143 | Received value is not a These. 144 | Expected Both: 145 | Left: 1 146 | Right: 2 147 | Received: undefined 148 | `); 149 | }); 150 | test('if received is a Some', () => { 151 | expect(() => expect(some(1)).toStrictEqualBoth(1, 1)).toThrowErrorMatchingInlineSnapshot(` 152 | expect(received).toStrictEqualBoth(expectedLeft, expectedRight) 153 | 154 | Received value is not a These. 155 | Expected Both: 156 | Left: 1 157 | Right: 1 158 | Received: {"_tag": "Some", "value": 1} 159 | `); 160 | }); 161 | test('if received is a basic type', () => { 162 | expect(() => expect(200).toStrictEqualBoth(404, 200)).toThrowErrorMatchingInlineSnapshot(` 163 | expect(received).toStrictEqualBoth(expectedLeft, expectedRight) 164 | 165 | Received value is not a These. 166 | Expected Both: 167 | Left: 404 168 | Right: 200 169 | Received: 200 170 | `); 171 | }); 172 | }); 173 | 174 | describe('.not.toStrictEqualBoth should pass', () => { 175 | test('if received is a Both that does not equal the expected value', () => { 176 | expect(both('Left', 'Right')).not.toStrictEqualBoth('A different left', 'A different right'); 177 | }); 178 | test('if received is a Left', () => { 179 | expect(left('Left')).not.toStrictEqualBoth('Left', 'Left'); 180 | }); 181 | test('if received is a Right', () => { 182 | expect(right('Hello')).not.toStrictEqualBoth('Hello', 'again'); 183 | }); 184 | test('if received is null', () => { 185 | expect(null).not.toStrictEqualBoth(null, null); 186 | }); 187 | test('if received is a None', () => { 188 | expect(none).not.toStrictEqualBoth(none, none); 189 | }); 190 | }); 191 | 192 | describe('.not.toEqualLeft should fail', () => { 193 | test('if received is a Both that strictly equals the expected value', () => { 194 | expect(() => expect(both('a', 'b')).not.toStrictEqualBoth('a', 'b')) 195 | .toThrowErrorMatchingInlineSnapshot(` 196 | expect(received).not.toStrictEqualBoth(expectedBoth) 197 | 198 | Expected Both: not 199 | Left: "a" 200 | Right: "b" 201 | `); 202 | }); 203 | }); 204 | -------------------------------------------------------------------------------- /src/eitherOrTheseMatchers/__tests__/toBeLeftErrorMatching.test.ts: -------------------------------------------------------------------------------- 1 | import { left as leftEither, right as rightEither } from 'fp-ts/lib/Either'; 2 | import { left as leftThese, right as rightThese, both } from 'fp-ts/lib/These'; 3 | import { some, none } from 'fp-ts/lib/Option'; 4 | import { toBeLeftErrorMatching } from '../../index'; 5 | 6 | expect.extend({ toBeLeftErrorMatching }); 7 | 8 | describe('.toBeLeftErrorMatching should pass', () => { 9 | test('if the received is a Left Either Error with the expected message', () => { 10 | expect(leftEither(new Error('egad'))).toBeLeftErrorMatching('egad'); 11 | }); 12 | test('if the received is a Left These Error with the expected message', () => { 13 | expect(leftThese(new Error('oops'))).toBeLeftErrorMatching('oops'); 14 | }); 15 | test('if the received is a Left Either Error with a message matching the supplied substring', () => { 16 | expect(leftEither(new Error('egad'))).toBeLeftErrorMatching('gad'); 17 | }); 18 | test('if the received is a Left These Error with a message matching the supplied regex', () => { 19 | expect(leftThese(new Error('oops'))).toBeLeftErrorMatching(/ps$/); 20 | }); 21 | test('if called as an asymmetric matcher', () => { 22 | expect( 23 | leftEither(new Error('Any sufficiently advanced technology is equivalent to magic.')), 24 | ).toEqual( 25 | expect.toBeLeftErrorMatching('Any sufficiently advanced technology is equivalent to magic.'), 26 | ); 27 | }); 28 | }); 29 | 30 | describe('.toBeLeftErrorMatching should fail', () => { 31 | test('if received is a Left Error whose message does not match the expected value', () => { 32 | expect(() => expect(leftThese(new Error('oops'))).toBeLeftErrorMatching('egad')) 33 | .toThrowErrorMatchingInlineSnapshot(` 34 | expect(received).toBeLeftErrorMatching(expectedErrorMessage) 35 | 36 | Expected Left Error: "egad" 37 | Received Left Error: "oops" 38 | `); 39 | }); 40 | test('if received is a Left Error whose message does not match the expected substring', () => { 41 | expect(() => expect(leftThese(new Error('egad'))).toBeLeftErrorMatching('egad!')) 42 | .toThrowErrorMatchingInlineSnapshot(` 43 | expect(received).toBeLeftErrorMatching(expectedErrorMessage) 44 | 45 | Expected Left Error: "egad!" 46 | Received Left Error: "egad" 47 | `); 48 | }); 49 | test('if received is a Left Error whose message does not match the expected regex', () => { 50 | expect(() => expect(leftEither(new Error('egad!'))).toBeLeftErrorMatching(/egad$/)) 51 | .toThrowErrorMatchingInlineSnapshot(` 52 | expect(received).toBeLeftErrorMatching(expectedErrorMessage) 53 | 54 | Expected Left Error: /egad$/ 55 | Received Left Error: "egad!" 56 | `); 57 | }); 58 | test('if received is a Left that does not contain an Error', () => { 59 | expect(() => expect(leftEither('whoa')).toBeLeftErrorMatching(/egad/)) 60 | .toThrowErrorMatchingInlineSnapshot(` 61 | expect(received).toBeLeftErrorMatching(expectedErrorMessage) 62 | 63 | Received Left value is not an Error. 64 | Expected Left Error: /egad/ 65 | Received Left: "whoa" 66 | `); 67 | }); 68 | test('if received is a Right', () => { 69 | expect(() => expect(rightEither(new Error('whoa'))).toBeLeftErrorMatching('whoa')) 70 | .toThrowErrorMatchingInlineSnapshot(` 71 | expect(received).toBeLeftErrorMatching(expectedErrorMessage) 72 | 73 | Received value is not a Left. 74 | Expected Left Error: "whoa" 75 | Received Right: [Error: whoa] 76 | `); 77 | }); 78 | test('if received is a Both', () => { 79 | expect(() => expect(both('uh', 'oh')).toBeLeftErrorMatching('uh-oh')) 80 | .toThrowErrorMatchingInlineSnapshot(` 81 | expect(received).toBeLeftErrorMatching(expectedErrorMessage) 82 | 83 | Received value is not a Left. 84 | Expected Left Error: "uh-oh" 85 | Received Both: 86 | Left: "uh" 87 | Right: "oh" 88 | `); 89 | }); 90 | }); 91 | 92 | describe('.toBeLeftErrorMatching should fail and indicate not an Either or These', () => { 93 | test('if received is undefined', () => { 94 | expect(() => expect(undefined).toBeLeftErrorMatching('oops')) 95 | .toThrowErrorMatchingInlineSnapshot(` 96 | expect(received).toBeLeftErrorMatching(expectedErrorMessage) 97 | 98 | Received value is not an Either or These. 99 | Expected Left Error: "oops" 100 | Received: undefined 101 | `); 102 | }); 103 | test('if received is a Some', () => { 104 | expect(() => expect(some('uh-oh')).toBeLeftErrorMatching('uh-oh')) 105 | .toThrowErrorMatchingInlineSnapshot(` 106 | expect(received).toBeLeftErrorMatching(expectedErrorMessage) 107 | 108 | Received value is not an Either or These. 109 | Expected Left Error: "uh-oh" 110 | Received: {"_tag": "Some", "value": "uh-oh"} 111 | `); 112 | }); 113 | test('if received is just a string', () => { 114 | expect(() => expect('egad').toBeLeftErrorMatching('egad')).toThrowErrorMatchingInlineSnapshot(` 115 | expect(received).toBeLeftErrorMatching(expectedErrorMessage) 116 | 117 | Received value is not an Either or These. 118 | Expected Left Error: "egad" 119 | Received: "egad" 120 | `); 121 | }); 122 | }); 123 | 124 | describe('.not.toBeLeftErrorMatching should pass', () => { 125 | test('if received is a Left Error that does not match the expected value', () => { 126 | expect(leftEither(new Error('gosh'))).not.toBeLeftErrorMatching('darn'); 127 | }); 128 | test('if received is a Right', () => { 129 | expect(rightThese('Hello')).not.toBeLeftErrorMatching('Hello'); 130 | }); 131 | test('if received is a Both', () => { 132 | expect(both('golly', 'golly')).not.toBeLeftErrorMatching('golly'); 133 | }); 134 | test('if received is null', () => { 135 | expect(null).not.toBeLeftErrorMatching('null'); 136 | }); 137 | test('if received is a None', () => { 138 | expect(none).not.toBeLeftErrorMatching('none'); 139 | }); 140 | }); 141 | 142 | describe('.not.toBeLeftErrorMatching should fail', () => { 143 | test('if received is a Left Either with an Error that exactly matches the expected string', () => { 144 | expect(() => expect(leftEither(new Error('gee'))).not.toBeLeftErrorMatching('gee')) 145 | .toThrowErrorMatchingInlineSnapshot(` 146 | expect(received).not.toBeLeftErrorMatching(expectedErrorMessage) 147 | 148 | Expected Left Error: not "gee" 149 | `); 150 | }); 151 | test('if received is a Left These with an Error that matches the expected substring', () => { 152 | expect(() => expect(leftThese(new Error('oh-no'))).not.toBeLeftErrorMatching('no')) 153 | .toThrowErrorMatchingInlineSnapshot(` 154 | expect(received).not.toBeLeftErrorMatching(expectedErrorMessage) 155 | 156 | Expected Left Error: not "no" 157 | Received Left Error: "oh-no" 158 | `); 159 | }); 160 | test('if received is a Left Either with an Error that matches the supplied regex', () => { 161 | expect(() => expect(leftEither(new Error('totally wrong'))).not.toBeLeftErrorMatching(/wrong/)) 162 | .toThrowErrorMatchingInlineSnapshot(` 163 | expect(received).not.toBeLeftErrorMatching(expectedErrorMessage) 164 | 165 | Expected Left Error: not /wrong/ 166 | Received Left Error: "totally wrong" 167 | `); 168 | }); 169 | test('if received is a Left These with an Error that matches the supplied regex', () => { 170 | expect(() => expect(leftThese(new Error('faux-pas'))).not.toBeLeftErrorMatching(/faux/)) 171 | .toThrowErrorMatchingInlineSnapshot(` 172 | expect(received).not.toBeLeftErrorMatching(expectedErrorMessage) 173 | 174 | Expected Left Error: not /faux/ 175 | Received Left Error: "faux-pas" 176 | `); 177 | }); 178 | }); 179 | -------------------------------------------------------------------------------- /src/eitherOrTheseMatchers/__tests__/toStrictEqualRight.test.ts: -------------------------------------------------------------------------------- 1 | import { left as leftEither, right as rightEither } from 'fp-ts/lib/Either'; 2 | import { left as leftThese, right as rightThese, both } from 'fp-ts/lib/These'; 3 | import { some, none } from 'fp-ts/lib/Option'; 4 | import { toStrictEqualRight } from '../../index'; 5 | 6 | expect.extend({ toStrictEqualRight }); 7 | 8 | class Message { 9 | message: string; 10 | constructor(message: string) { 11 | this.message = message; 12 | } 13 | } 14 | 15 | describe('.toStrictEqualRight should pass', () => { 16 | test('if the received is a Right Either with the expected value', () => { 17 | expect(rightEither('Expected Right')).toStrictEqualRight('Expected Right'); 18 | }); 19 | test('if the received is a Right These with the expected value', () => { 20 | expect(rightThese(42)).toStrictEqualRight(42); 21 | }); 22 | test('if received is a Right with an undefined expected value', () => { 23 | expect(rightEither(undefined)).toStrictEqualRight(undefined); 24 | }); 25 | test('if received is a Right with a null expected value', () => { 26 | expect(rightThese(null)).toStrictEqualRight(null); 27 | }); 28 | test('if called as an asymmetric matcher', () => { 29 | expect(rightEither('Any sufficiently advanced technology is equivalent to magic.')).toEqual( 30 | expect.toStrictEqualRight('Any sufficiently advanced technology is equivalent to magic.'), 31 | ); 32 | }); 33 | test('if called with an asymmetric matcher', () => { 34 | expect(rightThese('You affect the world by what you browse.')).toStrictEqualRight( 35 | expect.stringContaining('world'), 36 | ); 37 | }); 38 | }); 39 | 40 | describe('.toStrictEqualRight should fail and include the expected and received values', () => { 41 | test('if received is a Right that does not equal the expected value', () => { 42 | expect(() => expect(rightThese(404)).toStrictEqualRight(200)) 43 | .toThrowErrorMatchingInlineSnapshot(` 44 | expect(received).toStrictEqualRight(expectedRight) 45 | 46 | Expected Right: 200 47 | Received Right: 404 48 | `); 49 | }); 50 | test('if received is a Left', () => { 51 | expect(() => expect(leftEither('received left')).toStrictEqualRight('expected right')) 52 | .toThrowErrorMatchingInlineSnapshot(` 53 | expect(received).toStrictEqualRight(expectedRight) 54 | 55 | Expected Right: "expected right" 56 | Received Left: "received left" 57 | `); 58 | }); 59 | test('if received is a Both', () => { 60 | expect(() => 61 | expect(both('received left', 'received right')).toStrictEqualRight('expected right'), 62 | ).toThrowErrorMatchingInlineSnapshot(` 63 | expect(received).toStrictEqualRight(expectedRight) 64 | 65 | Expected Right: "expected right" 66 | Received Both: 67 | Left: "received left" 68 | Right: "received right" 69 | `); 70 | }); 71 | }); 72 | 73 | describe('.toStrictEqualRight should fail and show the difference', () => { 74 | test('if received is a Right that does not strictly equal the expected class instance', () => { 75 | expect(() => 76 | expect(rightEither({ message: 'Does not compute!' })).toStrictEqualRight( 77 | new Message('Does not compute!'), 78 | ), 79 | ).toThrowErrorMatchingInlineSnapshot(` 80 | expect(received).toStrictEqualRight(expectedRight) 81 | 82 | - Expected Right - 1 83 | + Received Right + 1 84 | 85 | - Message { 86 | + Object { 87 | "message": "Does not compute!", 88 | } 89 | `); 90 | }); 91 | test('if received is a Right sparse array that serializes to the same value as an expected non-sparse array', () => { 92 | // eslint-disable-next-line no-sparse-arrays 93 | expect(() => expect(rightThese([1, , 3])).toStrictEqualRight([1, undefined, 3])) 94 | .toThrowErrorMatchingInlineSnapshot(` 95 | expect(received).toStrictEqualRight(expectedRight) 96 | 97 | Expected Right: [1, undefined, 3] 98 | Received Right: [1, , 3] 99 | `); 100 | }); 101 | test('if received is a Right non-sparse array that serializes to the same value as an expected sparse array', () => { 102 | // eslint-disable-next-line no-sparse-arrays 103 | expect(() => expect(rightThese([1, undefined, 3])).toStrictEqualRight([1, , 3])) 104 | .toThrowErrorMatchingInlineSnapshot(` 105 | expect(received).toStrictEqualRight(expectedRight) 106 | 107 | Expected Right: [1, , 3] 108 | Received Right: [1, undefined, 3] 109 | `); 110 | }); 111 | }); 112 | 113 | describe('.toStrictEqualRight should fail and indicate not an Either or These', () => { 114 | test('if received is a Some', () => { 115 | expect(() => expect(some('my some')).toStrictEqualRight('expected right')) 116 | .toThrowErrorMatchingInlineSnapshot(` 117 | expect(received).toStrictEqualRight(expectedRight) 118 | 119 | Received value is not an Either or These. 120 | Expected Right: "expected right" 121 | Received: {"_tag": "Some", "value": "my some"} 122 | `); 123 | }); 124 | test('if received is a None', () => { 125 | expect(() => expect(none).toStrictEqualRight(none)).toThrowErrorMatchingInlineSnapshot(` 126 | expect(received).toStrictEqualRight(expectedRight) 127 | 128 | Received value is not an Either or These. 129 | Expected Right: {"_tag": "None"} 130 | Received: {"_tag": "None"} 131 | `); 132 | }); 133 | test('if received is a basic type', () => { 134 | expect(() => expect(200).toStrictEqualRight(200)).toThrowErrorMatchingInlineSnapshot(` 135 | expect(received).toStrictEqualRight(expectedRight) 136 | 137 | Received value is not an Either or These. 138 | Expected Right: 200 139 | Received: 200 140 | `); 141 | }); 142 | test('if received is null', () => { 143 | expect(() => expect(null).toStrictEqualRight(null)).toThrowErrorMatchingInlineSnapshot(` 144 | expect(received).toStrictEqualRight(expectedRight) 145 | 146 | Received value is not an Either or These. 147 | Expected Right: null 148 | Received: null 149 | `); 150 | }); 151 | test('if received is undefined', () => { 152 | expect(() => expect(undefined).toStrictEqualRight(undefined)) 153 | .toThrowErrorMatchingInlineSnapshot(` 154 | expect(received).toStrictEqualRight(expectedRight) 155 | 156 | Received value is not an Either or These. 157 | Expected Right: undefined 158 | Received: undefined 159 | `); 160 | }); 161 | }); 162 | 163 | describe('.not.toStrictEqualRight should pass', () => { 164 | test('if received is a Right that does not equal the expected value', () => { 165 | expect(rightEither('Right')).not.toStrictEqualRight('A different value'); 166 | }); 167 | test('if received is a Left', () => { 168 | expect(leftThese('Hello')).not.toStrictEqualRight('Hello'); 169 | }); 170 | test('if received is a Both', () => { 171 | expect(both('Right', 'Right')).not.toStrictEqualRight('Right'); 172 | }); 173 | test('if received is a Some', () => { 174 | expect(some('value')).not.toStrictEqualRight('value'); 175 | }); 176 | test('if received is a None', () => { 177 | expect(none).not.toStrictEqualRight(none); 178 | }); 179 | test('if received is null', () => { 180 | expect(null).not.toStrictEqualRight(null); 181 | }); 182 | test('if received is undefined', () => { 183 | expect(undefined).not.toStrictEqualRight(undefined); 184 | }); 185 | test('if received is a Right that does not strictly equal the expected sparse array', () => { 186 | // eslint-disable-next-line no-sparse-arrays 187 | expect(() => expect(rightThese([1, undefined, 3])).not.toStrictEqualRight([1, , 3])); 188 | }); 189 | test('if received is a Right that does not strictly equal the expected class instance', () => { 190 | expect(() => 191 | expect(rightEither({ message: 'Does not compute!' })).not.toStrictEqualRight( 192 | new Message('Does not compute!'), 193 | ), 194 | ); 195 | }); 196 | }); 197 | 198 | describe('.not.toStrictEqualRight should fail', () => { 199 | test('if received is a Right Either that equals the expected value', () => { 200 | expect(() => expect(rightEither('right')).not.toStrictEqualRight('right')) 201 | .toThrowErrorMatchingInlineSnapshot(` 202 | expect(received).not.toStrictEqualRight(expectedRight) 203 | 204 | Expected Right: not "right" 205 | `); 206 | }); 207 | test('if received is a Right These that equals the expected value', () => { 208 | expect(() => expect(rightThese(200)).not.toStrictEqualRight(200)) 209 | .toThrowErrorMatchingInlineSnapshot(` 210 | expect(received).not.toStrictEqualRight(expectedRight) 211 | 212 | Expected Right: not 200 213 | `); 214 | }); 215 | }); 216 | -------------------------------------------------------------------------------- /src/theseMatchers/__tests__/toSubsetEqualBoth.test.ts: -------------------------------------------------------------------------------- 1 | import { left, right, both } from 'fp-ts/lib/These'; 2 | import { some, none } from 'fp-ts/lib/Option'; 3 | import { toSubsetEqualBoth } from '../../index'; 4 | 5 | expect.extend({ toSubsetEqualBoth }); 6 | 7 | class Message { 8 | message: string; 9 | constructor(message: string) { 10 | this.message = message; 11 | } 12 | } 13 | 14 | describe('.toSubsetEqualBoth should pass', () => { 15 | test('if the received is a Both with the expected value', () => { 16 | expect(both('Left', 'Right')).toSubsetEqualBoth('Left', 'Right'); 17 | }); 18 | test('if received is a Both with undefined expected values', () => { 19 | expect(both(undefined, undefined)).toSubsetEqualBoth(undefined, undefined); 20 | }); 21 | test('if called as an asymmetric matcher', () => { 22 | expect( 23 | both('Any sufficiently advanced technology is equivalent to magic.', 'Arthur C. Clarke'), 24 | ).toEqual( 25 | expect.toSubsetEqualBoth( 26 | 'Any sufficiently advanced technology is equivalent to magic.', 27 | 'Arthur C. Clarke', 28 | ), 29 | ); 30 | }); 31 | test('if called with an asymmetric matcher', () => { 32 | expect(both('You affect the world by what you browse.', 'Tim Berners-Lee')).toSubsetEqualBoth( 33 | expect.stringContaining('world'), 34 | expect.any(String), 35 | ); 36 | }); 37 | test('if received is a Both that does not strictly equal the expected sparse array', () => { 38 | expect(() => 39 | // eslint-disable-next-line no-sparse-arrays 40 | expect(both([1, undefined, 3], [undefined, 2])).toSubsetEqualBoth([1, , 3], [, 2]), 41 | ); 42 | }); 43 | test('if received is a Both whose values do not strictly equal the expected class instances', () => { 44 | expect(() => 45 | expect(both({ message: 'Does not compute!' }, { message: 'All clear' })).toSubsetEqualBoth( 46 | new Message('Does not compute!'), 47 | new Message('All clear'), 48 | ), 49 | ); 50 | }); 51 | }); 52 | 53 | describe('.toSubsetEqualBoth should fail', () => { 54 | test('if received is a Both that does not equal the expected value', () => { 55 | expect(() => expect(both({ a: 1 }, { b: 2 })).toSubsetEqualBoth({ a: 2 }, { b: 2 })) 56 | .toThrowErrorMatchingInlineSnapshot(` 57 | expect(received).toSubsetEqualBoth(expectedLeft, expectedRight) 58 | 59 | Expected Both: 60 | Left: {"a": 2} 61 | Right: {"b": 2} 62 | 63 | Difference from Left: 64 | - Expected - 1 65 | + Received + 1 66 | 67 | Object { 68 | - "a": 2, 69 | + "a": 1, 70 | } 71 | `); 72 | }); 73 | test('if the received is a both that does not contain all of the expected properties', () => { 74 | expect(() => 75 | expect(both({ orderNumber: 100 }, 1)).toSubsetEqualBoth( 76 | { 77 | orderId: '123', 78 | orderNumber: 100, 79 | }, 80 | 81 | 1, 82 | ), 83 | ).toThrowErrorMatchingInlineSnapshot(` 84 | expect(received).toSubsetEqualBoth(expectedLeft, expectedRight) 85 | 86 | Expected Both: 87 | Left: {"orderId": "123", "orderNumber": 100} 88 | Right: 1 89 | 90 | Difference from Left: 91 | - Expected - 1 92 | + Received + 0 93 | 94 | Object { 95 | - "orderId": "123", 96 | "orderNumber": 100, 97 | } 98 | `); 99 | }); 100 | test('if the received is a Both with an array of objects that does not contain all of the expected objects', () => { 101 | expect(() => 102 | expect(both([{ a: 'a' }, { b: 'b' }], [{ d: 'd' }])).toSubsetEqualBoth( 103 | [{ a: 'a' }, { b: 'b' }, { c: 'c' }], 104 | [{ e: 'e' }], 105 | ), 106 | ).toThrowErrorMatchingInlineSnapshot(` 107 | expect(received).toSubsetEqualBoth(expectedLeft, expectedRight) 108 | 109 | Expected Both: 110 | Left: [{"a": "a"}, {"b": "b"}, {"c": "c"}] 111 | Right: [{"e": "e"}] 112 | 113 | Difference from Left: 114 | - Expected - 3 115 | + Received + 0 116 | 117 | Array [ 118 | Object { 119 | "a": "a", 120 | }, 121 | Object { 122 | "b": "b", 123 | }, 124 | - Object { 125 | - "c": "c", 126 | - }, 127 | ] 128 | 129 | Difference from Right: 130 | - Expected - 1 131 | + Received + 1 132 | 133 | Array [ 134 | Object { 135 | - "e": "e", 136 | + "d": "d", 137 | }, 138 | ] 139 | `); 140 | }); 141 | test('if the received is a Both with an array of objects that contains more than the expected objects', () => { 142 | expect(() => 143 | expect( 144 | both([{ num: 1 }, { num: 2 }, { num: 3 }], [{ str: 'a' }, { str: 'b' }]), 145 | ).toSubsetEqualBoth([{ num: 1 }, { num: 2 }], [{ str: 'a' }, { str: 'b' }]), 146 | ).toThrowErrorMatchingInlineSnapshot(` 147 | expect(received).toSubsetEqualBoth(expectedLeft, expectedRight) 148 | 149 | Expected Both: 150 | Left: [{"num": 1}, {"num": 2}] 151 | Right: [{"str": "a"}, {"str": "b"}] 152 | 153 | Difference from Left: 154 | - Expected - 0 155 | + Received + 3 156 | 157 | Array [ 158 | Object { 159 | "num": 1, 160 | }, 161 | Object { 162 | "num": 2, 163 | }, 164 | + Object { 165 | + "num": 3, 166 | + }, 167 | ] 168 | `); 169 | }); 170 | test('if received is a Left', () => { 171 | expect(() => expect(left('received left')).toSubsetEqualBoth('expected left', 'expected right')) 172 | .toThrowErrorMatchingInlineSnapshot(` 173 | expect(received).toSubsetEqualBoth(expectedLeft, expectedRight) 174 | 175 | Expected Both: 176 | Left: "expected left" 177 | Right: "expected right" 178 | 179 | Received Left: "received left" 180 | `); 181 | }); 182 | test('if received is a Right', () => { 183 | expect(() => 184 | expect(right('received right')).toSubsetEqualBoth('expected left', 'expected right'), 185 | ).toThrowErrorMatchingInlineSnapshot(` 186 | expect(received).toSubsetEqualBoth(expectedLeft, expectedRight) 187 | 188 | Expected Both: 189 | Left: "expected left" 190 | Right: "expected right" 191 | 192 | Received Right: "received right" 193 | `); 194 | }); 195 | }); 196 | 197 | describe('.toSubsetEqualBoth should fail and indicate not a These', () => { 198 | test('if received is undefined', () => { 199 | expect(() => expect(undefined).toSubsetEqualBoth(1, 2)).toThrowErrorMatchingInlineSnapshot(` 200 | expect(received).toSubsetEqualBoth(expectedLeft, expectedRight) 201 | 202 | Received value is not a These. 203 | Expected Both: 204 | Left: 1 205 | Right: 2 206 | Received: undefined 207 | `); 208 | }); 209 | test('if received is a Some', () => { 210 | expect(() => expect(some(1)).toSubsetEqualBoth(1, 1)).toThrowErrorMatchingInlineSnapshot(` 211 | expect(received).toSubsetEqualBoth(expectedLeft, expectedRight) 212 | 213 | Received value is not a These. 214 | Expected Both: 215 | Left: 1 216 | Right: 1 217 | Received: {"_tag": "Some", "value": 1} 218 | `); 219 | }); 220 | test('if received is a basic type', () => { 221 | expect(() => expect(200).toSubsetEqualBoth(404, 200)).toThrowErrorMatchingInlineSnapshot(` 222 | expect(received).toSubsetEqualBoth(expectedLeft, expectedRight) 223 | 224 | Received value is not a These. 225 | Expected Both: 226 | Left: 404 227 | Right: 200 228 | Received: 200 229 | `); 230 | }); 231 | }); 232 | 233 | describe('.not.toSubsetEqualBoth should pass', () => { 234 | test('if received is a Both that does not equal the expected value', () => { 235 | expect(both('Left', 'Right')).not.toSubsetEqualBoth('A different left', 'A different right'); 236 | }); 237 | test('if received is a Left', () => { 238 | expect(left('Left')).not.toSubsetEqualBoth('Left', 'Left'); 239 | }); 240 | test('if received is a Right', () => { 241 | expect(right('Hello')).not.toSubsetEqualBoth('Hello', 'again'); 242 | }); 243 | test('if received is null', () => { 244 | expect(null).not.toSubsetEqualBoth(null, null); 245 | }); 246 | test('if received is a None', () => { 247 | expect(none).not.toSubsetEqualBoth(none, none); 248 | }); 249 | }); 250 | 251 | describe('.not.toEqualLeft should fail', () => { 252 | test('if received is a Both that strictly equals the expected value', () => { 253 | expect(() => expect(both('a', 'b')).not.toSubsetEqualBoth('a', 'b')) 254 | .toThrowErrorMatchingInlineSnapshot(` 255 | expect(received).not.toSubsetEqualBoth(expectedBoth) 256 | 257 | Expected Both: not 258 | Left: "a" 259 | Right: "b" 260 | `); 261 | }); 262 | }); 263 | --------------------------------------------------------------------------------