@as-pect/assembly
Version:
Write AssemblyScript tests at blazing fast speeds
545 lines (471 loc) • 16.1 kB
text/typescript
import { toIncludeComparison } from "./comparison/toIncludeComparison";
import { toIncludeEqualComparison } from "./comparison/toIncludeEqualComparison";
import { Actual } from "./Actual";
import { Expected } from "./Expected";
import { assert } from "./assert";
// @ts-ignore: Decorators *are* valid here
declare function tryCall(func: () => void): bool;
// @ts-ignore: Decorators *are* valid here
export class Expectation<T> {
/**
* This i32 is set to 1 if the expectation is negated. Using the _not (xor) condition assertion
* makes assertions very easy to write and understand.
*/
_not: i32 = 0;
actual: T;
constructor(actual: T) {
this.actual = actual;
}
public get not(): Expectation<T> {
this._not = 1;
return this;
}
public toBe(expected: T, message: string = ""): void {
let actual = this.actual;
let equals = 0;
if (isReference<T>()) {
if (isNullable<T>()) {
if (changetype<usize>(actual) == 0 && changetype<usize>(expected) == 0) {
equals = 1;
} else {
if (
i32(changetype<usize>(actual) == 0)
^ i32(changetype<usize>(expected) == 0)
) {
equals = 0;
} else {
equals = i32(actual! == expected!);
}
}
} else {
equals = i32(actual == expected);
}
} else {
equals = i32(actual == expected);
}
let negated = this._not;
Actual.report(actual);
if (isReference(actual) && !isFunction(actual)) {
if (
!negated &&
changetype<usize>(actual) !== 0 &&
changetype<usize>(expected) !== 0 &&
Reflect.equals(actual, expected) == Reflect.SUCCESSFUL_MATCH
) {
Expected.report("Serializes to same value.", 0);
} else {
Expected.report(expected, negated);
}
} else {
Expected.report(expected, negated);
}
// The assertion is either the items equal, or the expectation is negated
assert(equals ^ negated, message);
Actual.clear();
Expected.clear();
}
public toStrictEqual(expected: T, message: string = ""): void {
let result = Reflect.FAILED_MATCH;
result = Reflect.equals(this.actual, expected);
let equals = i32(result == Reflect.SUCCESSFUL_MATCH);
Actual.report(this.actual);
Expected.report(expected);
assert(equals ^ this._not, message);
Actual.clear();
Expected.clear();
}
// @as-covers: ignore because this function is deprecated
public toBlockEqual(expected: T, message: string = ""): void {
WARNING(
"toBlockEqual has been deprecated and results in a toStrictEqual call.",
);
this.toStrictEqual(expected, message);
}
public toBeTruthy(message: string = ""): void {
let actual = this.actual;
Actual.report(actual);
let negated = this._not;
Expected.reportTruthy(negated);
if (isReference(actual)) {
if (actual instanceof String) {
let truthy = i32(
changetype<usize>(actual) != 0 &&
changetype<string>(actual).length > 0,
);
assert(truthy ^ negated, message);
} else {
let truthy = i32(changetype<usize>(actual) != 0);
assert(truthy ^ negated, message);
}
} else {
if (isFloat(actual)) {
let truthy = i32(!isNaN(actual) && actual != 0.0);
assert(truthy ^ negated, message);
} else if (isInteger(actual)) {
let truthy = i32(actual != 0);
assert(truthy ^ negated, message);
}
}
Actual.clear();
Expected.clear();
}
public toBeFalsy(message: string = ""): void {
let actual = this.actual;
Actual.report(actual);
let negated = this._not;
Expected.reportFalsy(negated);
if (isReference(actual)) {
// strings require an extra length check
if (actual instanceof String) {
let falsy = i32(
changetype<usize>(actual) == 0 ||
changetype<string>(actual).length == 0,
);
assert(falsy ^ negated, message);
} else {
let falsy = i32(changetype<usize>(actual) == 0);
assert(falsy ^ negated, message);
}
} else {
if (isFloat(actual)) {
// @ts-ignore: actual is a float value
let falsy = i32(isNaN(actual) || actual == 0.0);
assert(falsy ^ negated, message);
} else if (isInteger(actual)) {
let falsy = i32(actual == 0);
assert(falsy ^ negated, message);
}
}
Actual.clear();
Expected.clear();
}
public toThrow(message: string = ""): void {
let actual = this.actual;
let negated = this._not;
if (!isFunction(actual)) {
// @as-covers: ignore because this is a compile time error
ERROR(nameof<T>());
ERROR(
"Expectation#toThrow assertion called on actual T where T is not a function reference",
);
}
if (idof<T>() != idof<() => void>())
// @as-covers: ignore because this is a compile time error
ERROR(
"Expectation#toThrow assertion called on actual T where T is not a function reference with signature () => void",
);
// @ts-ignore: safe tryCall
let throws = i32(!tryCall(actual));
Actual.report(throws ? "Throws" : "Not Throws");
Expected.report("Throws", negated);
assert(negated ^ throws, message);
Actual.clear();
Expected.clear();
}
public toBeGreaterThan(expected: T, message: string = ""): void {
let actual = this.actual;
let negated = this._not;
Actual.report(actual);
Expected.report(expected, negated);
if (!isDefined(actual > expected))
// @as-covers: ignore because this is a compile time error
ERROR(
"Invalid call to toBeGreaterThan. Generic type T must have an operator implemented for the greaterThan (>) operation.",
);
if (isReference(actual)) {
// Perform reference type null checks
assert(
i32(changetype<usize>(expected) != 0),
"Value comparison fails, expected value is null.",
);
assert(
i32(changetype<usize>(actual) != 0),
"Value comparison fails, actual value is null.",
);
}
// Compare float types
if (isFloat(actual)) {
assert(
i32(!isNaN(expected)),
"Value comparison fails, expected value is NaN.",
);
assert(
i32(!isNaN(actual)),
"Value comparison fails, actual value is NaN.",
);
}
// do actual greater than comparison
if (isNullable(actual)) {
assert(negated ^ i32(actual! > expected!), message);
} else {
assert(negated ^ i32(actual > expected), message);
}
Actual.clear();
Expected.clear();
}
public toBeGreaterThanOrEqual(expected: T, message: string = ""): void {
let actual = this.actual;
let negated = this._not;
Actual.report(actual);
Expected.report(expected, negated);
if (!isDefined(actual >= expected))
// @as-covers: ignore because this is a compile time error
ERROR(
"Invalid call to toBeGreaterThanOrEqual. Generic type T must have an operator implemented for the greaterThanOrEqual (>=) operation.",
);
// null checks
if (isReference(actual)) {
assert(
i32(changetype<usize>(expected) != 0),
"Value comparison fails, expected value is null.",
);
assert(
i32(changetype<usize>(actual) != 0),
"Value comparison fails, actual value is null.",
);
}
// Compare float types
if (isFloat(actual)) {
assert(
i32(!isNaN(expected)),
"Value comparison fails, expected value is NaN.",
);
assert(
i32(!isNaN(actual)),
"Value comparison fails, actual value is NaN.",
);
}
// do actual greater than comparison
if (isNullable(actual)) {
assert(negated ^ i32(actual! >= expected!), message);
} else {
assert(negated ^ i32(actual >= expected), message);
}
Actual.clear();
Expected.clear();
}
public toBeLessThan(expected: T, message: string = ""): void {
let actual = this.actual;
let negated = this._not;
Actual.report(actual);
Expected.report(expected, negated);
if (!isDefined(actual < expected))
// @as-covers: ignore because this is a compile time error
ERROR(
"Invalid call to toBeLessThan. Generic type T must have an operator implemented for the lessThan (<) operation.",
);
// null checks
if (isReference(actual)) {
assert(
i32(changetype<usize>(expected) != 0),
"Value comparison fails, expected value is null.",
);
assert(
i32(changetype<usize>(actual) != 0),
"Value comparison fails, actual value is null.",
);
} else if (isFloat(actual)) {
assert(
i32(!isNaN(expected)),
"Value comparison fails, expected value is NaN.",
);
assert(
i32(!isNaN(actual)),
"Value comparison fails, actual value is NaN.",
);
}
// do actual less than comparison
if (isNullable(actual)) {
assert(negated ^ i32(actual! < expected!), message);
} else {
assert(negated ^ i32(actual < expected), message);
}
Actual.clear();
Expected.clear();
}
public toBeLessThanOrEqual(expected: T, message: string = ""): void {
let actual = this.actual;
let negated = this._not;
Actual.report(actual);
Expected.report(expected, negated);
if (!isDefined(actual <= expected))
// @as-covers: ignore because this is a compile time error
ERROR(
"Invalid call to toBeLessThanOrEqual. Generic type T must have an operator implemented for the lessThanOrEqual (<=) operation.",
);
// null checks
if (isReference(actual)) {
assert(
i32(changetype<usize>(expected) != 0),
"Value comparison fails, expected value is null.",
);
assert(
i32(changetype<usize>(actual) != 0),
"Value comparison fails, actual value is null.",
);
}
if (isFloat(actual)) {
assert(
i32(!isNaN(expected)),
"Value comparison fails, expected value is NaN.",
);
assert(
i32(!isNaN(actual)),
"Value comparison fails, actual value is NaN.",
);
}
// do actual less than comparison
if (isNullable(actual)) {
assert(negated ^ i32(actual! <= expected!), message);
} else {
assert(negated ^ i32(actual <= expected), message);
}
Actual.clear();
Expected.clear();
}
public toBeNull(message: string = ""): void {
let negated = this._not;
let actual = this.actual;
if (actual instanceof usize) {
Actual.report(actual);
Expected.report(<usize>0, negated);
// @ts-ignore: actual is instanceof number type
assert(negated ^ i32(actual == 0), message);
Actual.clear();
Expected.clear();
} else if (isReference(actual)) {
Actual.report(actual);
Expected.report(changetype<T>(0), negated);
assert(negated ^ i32(changetype<usize>(actual) == 0), message);
Actual.clear();
Expected.clear();
// @as-covers: ignore because this is a compile time error
} else {
ERROR(
"toBeNull assertion must be called with a reference type T or usize.",
);
}
}
public toBeCloseTo(
expected: T,
decimalPlaces: i32 = 2,
message: string = "",
): void {
let actual = this.actual;
let negated = this._not;
// must be called on a float T
if (!isFloat(actual))
// @as-covers: ignore because this is a compile time error
ERROR("toBeCloseTo must be called with a Float value type T.");
Actual.report(actual);
Expected.report(expected, negated);
// both actual and expected values must be finite
assert(
i32(isFinite(actual)),
"toBeCloseTo assertion fails because a actual value is not finite",
);
assert(
i32(isFinite(expected)),
"toBeCloseTo assertion fails because expected value is not finite.",
);
// calculated: `|expected - actual| < 1 / numberOfDigits`.
// @ts-ignore tooling errors because T does not extend a numeric value type. This compiles just fine.
let isClose = i32(abs(expected - actual) < Math.pow(10, -decimalPlaces));
assert(negated ^ isClose, message);
Actual.clear();
Expected.clear();
}
public toBeNaN(message: string = ""): void {
let actual = this.actual;
let negated = this._not;
// must be called on a float T
if (!isFloat(actual))
// @as-covers: ignore because this is a compile time error
ERROR("toBeNaN must be called with a Float value type T.");
Actual.report(actual);
// @ts-ignore: The compiler should pass bit count (64/32 bit float to the report function)
Expected.report<T>(NaN, negated);
let isNaNValue = i32(isNaN(actual));
assert(isNaNValue ^ negated, message);
Actual.clear();
Expected.clear();
}
public toBeFinite(message: string = ""): void {
let actual = this.actual;
let negated = this._not;
// must be called on a float T
if (!isFloat(actual))
// @as-covers: ignore because this is a compile time error
ERROR("toBeNaN must be called with a Float value type T.");
Actual.report(actual);
Expected.reportFinite(negated);
let isFiniteValue = i32(isFinite(actual));
assert(isFiniteValue ^ negated, message);
Actual.clear();
Expected.clear();
}
public toHaveLength(expected: i32, message: string = ""): void {
let actual = this.actual;
let negated = this._not;
let length = 0;
if (actual instanceof ArrayBuffer) {
length = actual.byteLength;
} else {
// @ts-ignore: This results in a compile time check for a length property with a better error message
if (!isDefined(actual.length))
// @as-covers: ignore because this is a compile time error
ERROR(
"toHaveLength cannot be called on type T where T.length is not defined.",
);
if (isNullable(actual)) {
// @ts-ignore: This results in a compile time check for a length property with a better error message
length = <i32>actual!.length;
} else {
// @ts-ignore: This results in a compile time check for a length property with a better error message
length = <i32>actual.length;
}
}
Actual.report(length);
Expected.report(expected, negated);
let lengthsEqual = i32(length == expected);
assert(lengthsEqual ^ negated, message);
Actual.clear();
Expected.clear();
}
public toInclude<U>(expected: U, message: string = ""): void {
toIncludeComparison<T, U>(this.actual, expected, this._not, message);
Actual.clear();
Expected.clear();
}
// @ts-ignore: valueof<T> requires that T extends something with an @operator("[]")
// @as-covers: ignore because this is just an alias function
public toContain(expected: valueof<T>, message: string = ""): void {
this.toInclude(expected, message);
}
public toIncludeEqual<U>(expected: U, message: string = ""): void {
toIncludeEqualComparison<T, U>(this.actual, expected, this._not, message);
Actual.clear();
Expected.clear();
}
// @as-covers: ignore because this is an alias
public toContainEqual<U>(expected: U, message: string = ""): void {
this.toIncludeEqual(expected, message);
}
public toMatchSnapshot(name: string | null = null): void {
assert(i32(!this._not), "Snapshots cannot be negated.");
Expected.reportSnapshot(this.actual, name);
}
}
// @ts-ignore: decorators *are* valid here
export function expect<T>(actual: T): Expectation<T> {
return new Expectation(actual);
}
// @ts-ignore: decorators *are* valid here
// @as-covers: ignore because this is deprecated
export function expectFn(cb: () => void): Expectation<() => void> {
WARNING("expectFn() has been deprecated. Use expect() instead.");
return new Expectation(cb);
}