@applicvision/js-toolbox
Version:
A collection of tools for modern JavaScript development
241 lines (205 loc) • 5.37 kB
JavaScript
import assert, { AssertionError } from 'node:assert/strict'
export class ExpectationError extends AssertionError {
file
}
export class Expectation {
#negated = false
#value
#propertyName
constructor(value) {
this.#value = value
}
get to() {
return this
}
get be() {
return this
}
get have() {
return this
}
get not() {
this.#negated = !this.#negated
return this
}
get #negateMessage() {
return this.#negated ? 'not ' : ''
}
property(propertyName) {
try {
this.#value[propertyName]
} catch (error) {
const err = new ExpectationError({
message: error.message,
stackStartFn: this.property
})
err.file = err.stack?.match(/\((.+)\)/)?.[1]
throw err
}
this.#propertyName = propertyName
return this
}
get #computedValue() {
if (this.#propertyName) {
return this.#value[this.#propertyName]
}
return this.#value
}
#pickAssert(standard, negated) {
return this.#negated ? negated : standard
}
get #equalAssert() {
return this.#negated ? assert.notEqual : assert.equal
}
#runAssertion(assertFunction, { message, caller, compareValue, value = this.#computedValue }) {
try {
if ([assert.ok].includes(assertFunction)) {
assertFunction(value, message)
} else {
assertFunction(value, compareValue, message)
}
} catch (error) {
const err = new ExpectationError({
message: error.message,
stackStartFn: this[caller]
})
err.file = err.stack?.match(/\((.+)\)/)?.[1]
throw err
}
}
async #runAsyncAssertion(assertFunction, { message, errorMatcher }) {
try {
await assertFunction(this.#computedValue, errorMatcher, message)
} catch (error) {
const err = new ExpectationError({
message: error.message
})
err.file = error.actual?.stack.match(/\(?file.+/)
throw err
}
}
equal(compareValue) {
this.#runAssertion(
this.#equalAssert, {
caller: 'equal',
compareValue
})
}
deepEqual(compareValue) {
this.#runAssertion(
this.#pickAssert(assert.deepEqual, assert.notDeepEqual), {
caller: 'deepEqual',
compareValue
})
}
length(expectedLength) {
const value = this.#computedValue.length
this.#runAssertion(this.#equalAssert, {
caller: 'length',
compareValue: expectedLength,
value,
message: `Expected a length ${this.#negated ? 'other than' : 'of'} ${expectedLength}, but got ${value}.`
})
}
greaterThan(compareLength) {
const value = this.#computedValue > compareLength
this.#runAssertion(this.#equalAssert, {
caller: 'greaterThan',
value,
compareValue: true,
message: `Expected ${this.#computedValue} ${this.#negateMessage}to be greater than ${compareLength}.`
})
}
lessThan(compareNumber) {
const value = this.#computedValue < compareNumber
this.#runAssertion(this.#equalAssert, {
caller: 'lessThan',
value,
compareValue: true,
message: `Expected ${this.#computedValue} ${this.#negateMessage}to be less than ${compareNumber}.`
})
}
size(compareValue) {
this.#runAssertion(this.#equalAssert, {
caller: 'size',
compareValue,
value: this.#computedValue.size
})
}
match(compareValue) {
this.#runAssertion(this.#pickAssert(assert.match, assert.doesNotMatch), {
caller: 'match',
compareValue,
})
}
a(expectedType) {
const value = this.#computedValue
if (typeof expectedType == 'string') {
this.#runAssertion(this.#equalAssert, {
caller: 'a',
compareValue: expectedType.toLowerCase(),
value: typeof value
})
} else {
this.#runAssertion(this.#equalAssert, {
caller: 'a',
compareValue: true,
value: value instanceof expectedType,
message: `Expected ${this.#negateMessage}a ${expectedType.name}, but got ${value?.constructor.name ?? value}.`
})
}
}
resolve(errorMatcher) {
return this.#runAsyncAssertion(this.#pickAssert(assert.doesNotReject, assert.rejects), {
errorMatcher,
message: `Expected ${this.#computedValue} ${this.#negateMessage}to resolve.`
})
}
reject(errorMatcher = undefined) {
return this.#runAsyncAssertion(this.#pickAssert(assert.rejects, assert.doesNotReject), {
errorMatcher,
message: `Expected ${this.#computedValue} ${this.#negateMessage}to reject.`
})
}
empty() {
const value = this.#computedValue
const lengthOrSize = value.length ?? value.size ?? Object.keys(value ?? 0).length
this.#runAssertion(this.#equalAssert, {
compareValue: 0,
value: lengthOrSize,
caller: 'empty',
message: `Expected ${value} ${this.#negateMessage}to be empty.`
})
}
false() {
this.#runAssertion(this.#equalAssert, {
compareValue: false,
caller: 'false',
message: `Expected ${this.#computedValue} ${this.#negateMessage}to be false`
})
}
true() {
this.#runAssertion(this.#equalAssert, {
compareValue: true,
caller: 'true',
message: `Expected ${this.#computedValue} ${this.#negateMessage}to be true`
})
}
undefined() {
this.#runAssertion(this.#equalAssert, {
compareValue: undefined,
caller: 'undefined',
message: `Expected ${this.#computedValue} ${this.#negateMessage}to be undefined`
})
}
ok() {
this.#runAssertion(assert.ok, {
caller: 'ok',
message: `Expected ${this.#computedValue} ${this.#negateMessage}to be truthy`
})
}
}
Expectation.prototype.an = Expectation.prototype.a
export default function expect(value) {
return new Expectation(value)
}