Skip to main content
Module

x/expect/matchers.ts

helpers for writing jest like expect tests in deno
Latest
File
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628
import { AssertionError, equal} from 'https://deno.land/std@0.97.0/testing/asserts.ts'
import { diff, DiffResult, DiffType} from 'https://deno.land/std@0.97.0/testing/_diff.ts'
import { bold, gray, green, red, white} from 'https://deno.land/std@0.97.0/fmt/colors.ts'
import * as mock from './mock.ts'
type MatcherState = { isNot: boolean}
export type Matcher = (value: any, ...args: any[]) => MatchResult
export type Matchers = { [key: string]: Matcher}export type MatchResult = { pass: boolean message?: string}
const ACTUAL = red(bold('actual'))const EXPECTED = green(bold('expected'))
const CAN_NOT_DISPLAY = '[Cannot display]'
function createStr(v: unknown): string { try { return Deno.inspect(v) } catch (e) { return red(CAN_NOT_DISPLAY) }}
function createColor(diffType: DiffType): (s: string) => string { switch (diffType) { case DiffType.added: return (s: string) => green(bold(s)) case DiffType.removed: return (s: string) => red(bold(s)) default: return white }}
function createSign(diffType: DiffType): string { switch (diffType) { case DiffType.added: return '+ ' case DiffType.removed: return '- ' default: return ' ' }}
function buildMessage(diffResult: ReadonlyArray<DiffResult<string>>): string { return diffResult .map((result: DiffResult<string>) => { const c = createColor(result.type) return c(`${createSign(result.type)}${result.value}`) }) .join('\n')}
function buildDiffMessage(actual: unknown, expected: unknown) { const actualString = createStr(actual) const expectedString = createStr(expected)
let message try { const diffResult = diff( actualString.split('\n'), expectedString.split('\n') )
return buildMessage(diffResult) } catch (e) { return `\n${red(CAN_NOT_DISPLAY)} + \n\n` }}
function buildFail(message: string) { return { pass: false, message }}
export function toBe(actual: any, expected: any): MatchResult { if (actual === expected) return { pass: true }
return buildFail( `expect(${ACTUAL}).toBe(${EXPECTED})\n\n${buildDiffMessage( actual, expected )}` )}
export function toEqual(actual: any, expected: any): MatchResult { if (equal(actual, expected)) return { pass: true }
return buildFail( `expect(${ACTUAL}).toEqual(${EXPECTED})\n\n${buildDiffMessage( actual, expected )}` )}
export function toBeGreaterThan(actual: any, comparison: number): MatchResult { if (actual > comparison) return { pass: true }
const actualString = createStr(actual) const comparisonString = createStr(comparison)
return buildFail( `expect(${ACTUAL}).toBeGreaterThan(${EXPECTED})\n\n ${red( actualString )} is not greater than ${green(comparisonString)}` )}
export function toBeLessThan(actual: any, comparison: number): MatchResult { if (actual < comparison) return { pass: true }
const actualString = createStr(actual) const comparisonString = createStr(comparison)
return buildFail( `expect(${ACTUAL}).toBeLessThan(${EXPECTED})\n\n ${red( actualString )} is not less than ${green(comparisonString)}` )}
export function toBeGreaterThanOrEqual( actual: any, comparison: number): MatchResult { if (actual >= comparison) return { pass: true }
const actualString = createStr(actual) const comparisonString = createStr(comparison)
return buildFail( `expect(${ACTUAL}).toBeGreaterThanOrEqual(${EXPECTED})\n\n ${red( actualString )} is not greater than or equal to ${green(comparisonString)}` )}
export function toBeLessThanOrEqual( actual: any, comparison: number): MatchResult { if (actual <= comparison) return { pass: true }
const actualString = createStr(actual) const comparisonString = createStr(comparison)
return buildFail( `expect(${ACTUAL}).toBeLessThanOrEqual(${EXPECTED})\n\n ${red( actualString )} is not less than or equal to ${green(comparisonString)}` )}
export function toBeTruthy(value: any): MatchResult { if (value) return { pass: true }
const actualString = createStr(value)
return buildFail(`expect(${ACTUAL}).toBeTruthy()
${red(actualString)} is not truthy`)}
export function toBeTypeOf( value: any, expectedType: | "bigint" | "boolean" | "function" | "number" | "object" | "string" | "symbol" | "undefined",): MatchResult { const valueString = createStr(value); if ( [ "bigint", "boolean", "function", "number", "object", "string", "symbol", "undefined", ].includes(expectedType) ) { return typeof value === expectedType ? { pass: true } : buildFail( `expect(${ACTUAL}).toBeTypeOf(${EXPECTED})\n\n ${ red(valueString) } is not a ${expectedType}`, ); } return buildFail( `expect(${ACTUAL}).toBeTypeOf(${EXPECTED})\n\n ${ red(expectedType) } is not a known type to evaluate }`, );}
export function toBeFalsy(value: any): MatchResult { if (!value) return { pass: true }
const actualString = createStr(value)
return buildFail( `expect(${ACTUAL}).toBeFalsy()\n\n ${red(actualString)} is not falsy` )}
export function toBeDefined(value: unknown): MatchResult { if (typeof value !== 'undefined') return { pass: true }
const actualString = createStr(value)
return buildFail( `expect(${ACTUAL}).toBeDefined()\n\n ${red(actualString)} is not defined` )}
export function toBeUndefined(value: unknown): MatchResult { if (typeof value === 'undefined') return { pass: true }
const actualString = createStr(value)
return buildFail( `expect(${ACTUAL}).toBeUndefined()\n\n ${red( actualString )} is defined but should be undefined` )}
export function toBeNull(value: unknown): MatchResult { if (value === null) return { pass: true }
const actualString = createStr(value)
return buildFail( `expect(${ACTUAL}).toBeNull()\n\n ${red(actualString)} should be null` )}
export function toBeNaN(value: unknown): MatchResult { if (typeof value === 'number' && isNaN(value)) return { pass: true }
const actualString = createStr(value)
return buildFail( `expect(${ACTUAL}).toBeNaN()\n\n ${red(actualString)} should be NaN` )}
export function toBeInstanceOf(value: any, expected: Function): MatchResult { if (value instanceof expected) return { pass: true }
const actualString = createStr(value) const expectedString = createStr(expected)
return buildFail( `expect(${ACTUAL}).toBeInstanceOf(${EXPECTED})\n\n expected ${green( expected.name )} but received ${red(actualString)}` )}
export function toMatch(value: any, pattern: RegExp | string): MatchResult { const valueStr = value.toString() if (typeof pattern === 'string') { if (valueStr.indexOf(pattern) !== -1) return { pass: true }
const actualString = createStr(value) const patternString = createStr(pattern)
return buildFail( `expect(${ACTUAL}).toMatch(${EXPECTED})\n\n expected ${red( actualString )} to contain ${green(patternString)}` ) } else if (pattern instanceof RegExp) { if (pattern.exec(valueStr)) return { pass: true }
const actualString = createStr(value) const patternString = createStr(pattern)
return buildFail( `expect(${ACTUAL}).toMatch(${EXPECTED})\n\n ${red( actualString )} did not match regex ${green(patternString)}` ) } else { return buildFail('Invalid internal state') }}
export function toHaveProperty(value: any, propName: string, propValue?: any): MatchResult { if (typeof value === 'object' && typeof value[propName] !== 'undefined') { if (typeof propValue !== 'undefined') { return toHavePropertyWithValue(value, propName, propValue); } return { pass: true } }
const actualString = createStr(value) const propNameString = createStr(propName)
return buildFail( `expect(${ACTUAL}).toHaveProperty(${EXPECTED})\n\n ${red( actualString )} did not contain property ${green(propNameString)}` )}
function toHavePropertyWithValue(value: any, propName: string, propValue: any): MatchResult { const propValueString = createStr(propValue); const propNameString = createStr(propName); const actualString = createStr(value[propName]);
if (typeof value === 'object' && equal(value[propName], propValue)) { return { pass: true }; }
const VALUE = green(bold('value'));
return buildFail( `expect(${ACTUAL}).toHaveProperty(${EXPECTED}, ${VALUE})\n\n expected property ${green(propNameString)} to equal ${green(propValueString)} but was ${red(actualString)}` );}
export function toHaveLength(value: any, length: number): MatchResult { if (value?.length === length) return { pass: true }
const actualString = createStr(value.length) const lengthString = createStr(length)
return buildFail( `expect(${ACTUAL}).toHaveLength(${EXPECTED})\n\n expected array to have length ${green( lengthString )} but was ${red(actualString)}` )}
export function toContain(value: any, item: any): MatchResult { if (value && typeof value.includes === 'function' && value.includes(item)) { return { pass: true } }
const actualString = createStr(value) const itemString = createStr(item)
if (value && typeof value.includes === 'function') { return buildFail( `expect(${ACTUAL}).toContain(${EXPECTED})\n\n ${red( actualString )} did not contain ${green(itemString)}` ) } else { return buildFail( `expect(${ACTUAL}).toContain(${EXPECTED})\n\n expected ${red( actualString )} to have an includes method but it is ${green(itemString)}` ) }}
export function toThrow(value: any, error?: RegExp | string): MatchResult { let fn if (typeof value === 'function') { fn = value try { value = value() } catch (err) { value = err } }
const actualString = createStr(fn) const errorString = createStr(error)
if (value instanceof Error) { if (typeof error === 'string') { if (!value.message.includes(error)) { return buildFail( `expect(${ACTUAL}).toThrow(${EXPECTED})\n\nexpected ${red( actualString )} to throw error matching ${green(errorString)} but it threw ${red( value.toString() )}` ) } } else if (error instanceof RegExp) { if (!value.message.match(error)) { return buildFail( `expect(${ACTUAL}).toThrow(${EXPECTED})\n\nexpected ${red( actualString )} to throw error matching ${green(errorString)} but it threw ${red( value.toString() )}` ) } }
return { pass: true } } else { return buildFail( `expect(${ACTUAL}).toThrow(${EXPECTED})\n\nexpected ${red( actualString )} to throw but it did not` ) }}
function extractMockCalls( value: any, name: string): { error?: string; calls: mock.MockCall[] | null } { if (typeof value !== 'function') { return { calls: null, error: `${name} only works on mock functions. received: ${value}` } } const calls = mock.calls(value) if (calls === null) { return { calls: null, error: `${name} only works on mock functions` } }
return { calls }}
export function toHaveBeenCalled(value: any): MatchResult { const { calls, error } = extractMockCalls(value, 'toHaveBeenCalled') if (error) return buildFail(error)
const actualString = createStr(value)
if (calls && calls.length !== 0) return { pass: true }
return buildFail( `expect(${ACTUAL}).toHaveBeenCalled()\n\n ${red( actualString )} was not called` )}
export function toHaveBeenCalledTimes(value: any, times: number): MatchResult { const { calls, error } = extractMockCalls(value, 'toHaveBeenCalledTimes') if (error) return buildFail(error) if (!calls) return buildFail('Invalid internal state')
if (calls && calls.length === times) return { pass: true }
return buildFail( `expect(${ACTUAL}).toHaveBeenCalledTimes(${EXPECTED})\n\n expected ${times} calls but was called: ${calls.length}` )}
export function toHaveBeenCalledWith(value: any, ...args: any[]): MatchResult { const { calls, error } = extractMockCalls(value, 'toHaveBeenCalledWith') if (error) return buildFail(error)
const wasCalledWith = calls && calls.some((c) => equal(c.args, args)) if (wasCalledWith) return { pass: true }
const argsString = createStr(args)
return buildFail( `expect(${ACTUAL}).toHaveBeenCalledWith(${EXPECTED})\n\n function was not called with: ${green( argsString )}` )}
export function toHaveBeenLastCalledWith( value: any, ...args: any[]): MatchResult { const { calls, error } = extractMockCalls(value, 'toHaveBeenLastCalledWith') if (error) return buildFail(error)
if (!calls || !calls.length) { return buildFail( `expect(${ACTUAL}).toHaveBeenLastCalledWith(...${EXPECTED})\n\n expect last call args to be ${args} but was not called` ) }
const lastCall = calls[calls.length - 1] if (equal(lastCall.args, args)) return { pass: true }
return buildFail( `expect(${ACTUAL}).toHaveBeenLastCalledWith(...${EXPECTED})\n\n expect last call args to be ${args} but was: ${lastCall.args}` )}
export function toHaveBeenNthCalledWith( value: any, nth: number, ...args: any[]): MatchResult { const { calls, error } = extractMockCalls(value, 'toHaveBeenNthCalledWith') if (error) return buildFail(error)
const nthCall = calls && calls[nth - 1] if (nthCall) { if (equal(nthCall.args, args)) return { pass: true } return buildFail( `expect(${ACTUAL}).toHaveBeenNthCalledWith(${EXPECTED})\n\n expect ${nth}th call args to be ${args} but was: ${nthCall.args}` ) } else { return buildFail( `expect(${ACTUAL}).toHaveBeenNthCalledWith(${EXPECTED})\n\n ${nth}th call was not made.` ) }}
export function toHaveReturnedWith(value: any, result: any): MatchResult { const { calls, error } = extractMockCalls(value, 'toHaveReturnedWith') if (error) return buildFail(error)
const wasReturnedWith = calls && calls.some((c) => c.returns && equal(c.returned, result)) if (wasReturnedWith) return { pass: true }
return buildFail( `expect(${ACTUAL}).toHaveReturnedWith(${EXPECTED})\n\n function did not return: ${result}` )}
export function toHaveReturned(value: any): MatchResult { const { calls, error } = extractMockCalls(value, 'toHaveReturned') if (error) return buildFail(error)
if (calls && calls.some((c) => c.returns)) return { pass: true }
// TODO(allain): better messages return buildFail(`expected function to return but it never did`)}
// TODO(allain): better messagesexport function toHaveLastReturnedWith(value: any, expected: any): MatchResult { const { calls, error } = extractMockCalls(value, 'toHaveLastReturnedWith') if (error) return buildFail(error)
const lastCall = calls && calls[calls.length - 1] if (!lastCall) { return buildFail('no calls made to function') } if (lastCall.throws) { return buildFail(`last call to function threw: ${lastCall.thrown}`) }
if (equal(lastCall.returned, expected)) return { pass: true }
return buildFail( `expected last call to return ${expected} but returned: ${lastCall.returned}` )}
export function toHaveReturnedTimes(value: any, times: number): MatchResult { const { calls, error } = extractMockCalls(value, 'toHaveReturnedTimes') if (error) return buildFail(error)
const returnCount = calls && calls.filter((c) => c.returns).length if (returnCount !== times) { return buildFail( `expected ${times} returned times but returned ${returnCount} times` ) }
return { pass: true }}
export function toHaveNthReturnedWith( value: any, nth: number, expected: any): MatchResult { const { calls, error } = extractMockCalls(value, 'toHaveNthReturnedWith') if (error) return buildFail(error)
const nthCall = calls && calls[nth - 1] if (!nthCall) { return buildFail(`${nth} calls were now made`) }
if (nthCall.throws) { return buildFail(`${nth}th call to function threw: ${nthCall.thrown}`) }
if (!equal(nthCall.returned, expected)) { return buildFail( `expected ${nth}th call to return ${expected} but returned: ${nthCall.returned}` ) }
return { pass: true }}