Browse Source

integrate expect library

set_equality
Michaël Lemaire 10 months ago
parent
commit
5e2072c551
  1. 119
      expect/expect.ts
  2. 741
      expect/expect_test.ts
  3. 606
      expect/matchers.ts
  4. 717
      expect/matchers_test.ts
  5. 57
      expect/mock.ts
  6. 71
      expect/mock_test.ts
  7. 13
      normalize.ts
  8. 4
      testing.ts

119
expect/expect.ts

@ -0,0 +1,119 @@
import { AssertionError } from "https://deno.land/std@0.78.0/testing/asserts.ts";
import type { Matcher, Matchers } from "./matchers.ts";
import * as builtInMatchers from "./matchers.ts";
interface Expected {
toBe(candidate: any): void;
toEqual(candidate: any): void;
toBeTruthy(): void;
toBeFalsy(): void;
toBeDefined(): void;
toBeInstanceOf(clazz: any): void;
toBeUndefined(): void;
toBeNull(): void;
toBeNaN(): void;
toMatch(pattern: RegExp | string): void;
toHaveProperty(propName: string): void;
toHaveLength(length: number): void;
toContain(item: any): void;
toThrow(error?: RegExp | string): void;
toBeGreaterThan(number: number): void;
toBeGreaterThanOrEqual(number: number): void;
toBeLessThan(number: number): void;
toBeLessThanOrEqual(number: number): void;
toHaveBeenCalled(): void;
toHaveBeenCalledTimes(number: number): void;
toHaveBeenCalledWith(...args: any[]): void;
toHaveBeenLastCalledWith(...args: any[]): void;
toHaveBeenNthCalledWith(nthCall: number, ...args: any[]): void;
toHaveReturned(): void;
toHaveReturnedTimes(number: number): void;
toHaveReturnedWith(value: any): void;
toHaveLastReturnedWith(value: any): void;
toHaveNthReturnedWith(nthCall: number, value: any): void;
not: Expected;
resolves: Expected;
rejects: Expected;
}
const matchers: Record<any, Matcher> = {
...builtInMatchers,
};
export function expect(value: any): Expected {
let isNot = false;
let isPromised = false;
const self: any = new Proxy(
{},
{
get(_, name) {
if (name === "not") {
isNot = !isNot;
return self;
}
if (name === "resolves") {
if (!(value instanceof Promise)) {
throw new AssertionError("expected value must be a Promise");
}
isPromised = true;
return self;
}
if (name === "rejects") {
if (!(value instanceof Promise)) {
throw new AssertionError("expected value must be a Promise");
}
value = value.then(
(value) => {
throw new AssertionError(
`Promise did not reject. resolved to ${value}`,
);
},
(err) => err,
);
isPromised = true;
return self;
}
const matcher: Matcher = matchers[name as any];
if (!matcher) {
throw new TypeError(
typeof name === "string"
? `matcher not found: ${name}`
: "matcher not found",
);
}
return (...args: any[]) => {
function applyMatcher(value: any, args: any[]) {
if (isNot) {
let result = matcher(value, ...args);
if (result.pass) {
throw new AssertionError("should not " + result.message);
}
} else {
let result = matcher(value, ...args);
if (!result.pass) {
throw new AssertionError(result.message || "Unknown error");
}
}
}
return isPromised
? value.then((value: any) => applyMatcher(value, args))
: applyMatcher(value, args);
};
},
},
);
return self;
}
export function addMatchers(newMatchers: Matchers): void {
Object.assign(matchers, newMatchers);
}

741
expect/expect_test.ts

@ -0,0 +1,741 @@
import {
assertEquals,
AssertionError,
assertThrows,
} from "https://deno.land/std@0.78.0/testing/asserts.ts";
import { addMatchers, expect } from "./expect.ts";
import * as mock from "./mock.ts";
async function assertPass(fn: Function) {
try {
assertEquals(await fn(), undefined);
} catch (err) {
throw new AssertionError(
`expected ${fn.toString()} to pass but it failed with ${err}`,
);
}
}
async function assertAllPass(...fns: Function[]) {
for (let fn of fns) {
await assertPass(fn);
}
}
async function assertFail(fn: Function) {
let thrown = true;
try {
let resolution = await fn();
thrown = false;
throw new AssertionError(
`expected ${fn.toString()} to throw but it resolved with ${resolution}`,
);
} catch (err) {
// expected
if (!thrown) throw err;
}
}
async function assertAllFail(...fns: Function[]) {
for (let fn of fns) {
await assertFail(fn);
}
}
Deno.test({
name: "exportsFunction",
fn: () => {
assertEquals(typeof expect, "function");
},
});
Deno.test({
name: "throwsWhenNoMatcherFound",
fn: () => {
assertThrows(
//@ts-ignore
() => expect(true).toBeFancy(),
TypeError,
"matcher not found: toBeFancy",
);
},
});
Deno.test({
name: "allowsExtendingMatchers",
fn: () => {
addMatchers({
toBeFancy(value: any) {
if (value === "fancy") {
return { pass: true };
} else {
return { pass: false, message: "was not fancy" };
}
},
});
// @ts-ignore
assertPass(() => expect("fancy").toBeFancy());
},
});
Deno.test({
name: "toBe",
fn: async () => {
const obj = {};
assertEquals(typeof expect(obj).toBe, "function");
await assertAllPass(
() => expect(obj).toBe(obj),
() => expect(obj).not.toBe({}),
() => expect(Promise.resolve(1)).resolves.toBe(1),
() => expect(Promise.reject(1)).rejects.toBe(1),
);
await assertFail(() => expect(obj).toBe({}));
await assertFail(() => expect(obj).not.toBe(obj));
},
});
Deno.test({
name: "toEqual",
fn: async () => {
const obj = {};
await assertAllPass(
() => expect(1).toEqual(1),
() => expect(obj).toEqual({}),
() => expect(obj).toEqual(obj),
() => expect({ a: 1 }).toEqual({ a: 1 }),
() => expect([1]).toEqual([1]),
() => expect(Promise.resolve(1)).resolves.toEqual(1),
() => expect(Promise.reject(1)).rejects.toEqual(1),
);
await assertAllFail(
() => expect(1).toEqual(2),
() => expect(1).toEqual(true),
() => expect({}).toEqual(true),
() => expect(1).not.toEqual(1),
() => expect(true).not.toEqual(true),
);
},
});
Deno.test({
name: "resolves",
fn: async () => {
const resolves = expect(Promise.resolve(true)).resolves;
for (let method of ["toEqual", "toBe", "toBeTruthy", "toBeFalsy"]) {
assertEquals(
typeof (resolves as any)[method],
"function",
`missing ${method}`,
);
}
},
});
Deno.test({
name: "rejects",
fn: async () => {
const rejects = expect(Promise.reject(true)).rejects;
for (
let method of ["toEqual", "toBe", "toBeTruthy", "toBeFalsy"]
) {
assertEquals(typeof (rejects as any)[method], "function");
}
},
});
Deno.test({
name: "toBeDefined",
fn: async () => {
await assertAllPass(
() => expect(true).toBeDefined(),
() => expect({}).toBeDefined(),
() => expect([]).toBeDefined(),
() => expect(undefined).not.toBeDefined(),
() => expect(Promise.resolve({})).resolves.toBeDefined(),
() => expect(Promise.reject({})).rejects.toBeDefined(),
);
await assertAllFail(
() => expect(undefined).toBeDefined(),
() => expect(true).not.toBeDefined(),
);
},
});
Deno.test({
name: "toBeUndefined",
fn: async () => {
await assertAllPass(
() => expect(undefined).toBeUndefined(),
() => expect(null).not.toBeUndefined(),
() => expect(Promise.resolve(undefined)).resolves.toBeUndefined(),
() => expect(Promise.reject(undefined)).rejects.toBeUndefined(),
);
await assertAllFail(
() => expect(null).toBeUndefined(),
() => expect(undefined).not.toBeUndefined(),
() => expect(false).toBeUndefined(),
);
},
});
Deno.test({
name: "toBeTruthy",
fn: async () => {
await assertAllPass(
() => expect(true).toBeTruthy(),
() => expect(false).not.toBeTruthy(),
() => expect(Promise.resolve(true)).resolves.toBeTruthy(),
() => expect(Promise.reject(true)).rejects.toBeTruthy(),
);
await assertAllFail(
() => expect(false).toBeTruthy(),
() => expect(true).not.toBeTruthy(),
);
},
});
Deno.test({
name: "toBeFalsy",
fn: async () => {
await assertAllPass(
() => expect(false).toBeFalsy(),
() => expect(true).not.toBeFalsy(),
() => expect(Promise.resolve(false)).resolves.toBeFalsy(),
() => expect(Promise.reject(false)).rejects.toBeFalsy(),
);
await assertAllFail(
() => expect(true).toBeFalsy(),
() => expect(false).not.toBeFalsy(),
);
},
});
Deno.test({
name: "toBeGreaterThan",
fn: async () => {
await assertAllPass(
() => expect(2).toBeGreaterThan(1),
() => expect(1).not.toBeGreaterThan(2),
() => expect(Promise.resolve(2)).resolves.toBeGreaterThan(1),
() => expect(Promise.reject(2)).rejects.toBeGreaterThan(1),
);
await assertAllFail(
() => expect(1).toBeGreaterThan(1),
() => expect(1).toBeGreaterThan(2),
() => expect(2).not.toBeGreaterThan(1),
);
},
});
Deno.test({
name: "toBeLessThan",
fn: async () => {
await assertAllPass(
() => expect(1).toBeLessThan(2),
() => expect(2).not.toBeLessThan(1),
() => expect(Promise.resolve(1)).resolves.toBeLessThan(2),
() => expect(Promise.reject(1)).rejects.toBeLessThan(2),
);
await assertAllFail(
() => expect(1).toBeLessThan(1),
() => expect(2).toBeLessThan(1),
() => expect(1).not.toBeLessThan(2),
);
},
});
Deno.test({
name: "toBeGreaterThanOrEqual",
fn: async () => {
await assertAllPass(
() => expect(2).toBeGreaterThanOrEqual(1),
() => expect(1).toBeGreaterThanOrEqual(1),
() => expect(1).not.toBeGreaterThanOrEqual(2),
() => expect(Promise.resolve(2)).resolves.toBeGreaterThanOrEqual(2),
() => expect(Promise.reject(2)).rejects.toBeGreaterThanOrEqual(2),
);
await assertAllFail(
() => expect(1).toBeGreaterThanOrEqual(2),
() => expect(2).not.toBeGreaterThanOrEqual(1),
);
},
});
Deno.test({
name: "toBeLessThanOrEqual",
fn: async () => {
await assertAllPass(
() => expect(1).toBeLessThanOrEqual(2),
() => expect(1).toBeLessThanOrEqual(1),
() => expect(2).not.toBeLessThanOrEqual(1),
() => expect(Promise.resolve(1)).resolves.toBeLessThanOrEqual(2),
() => expect(Promise.reject(1)).rejects.toBeLessThanOrEqual(2),
);
await assertAllFail(
() => expect(2).toBeLessThanOrEqual(1),
() => expect(1).not.toBeLessThanOrEqual(1),
() => expect(1).not.toBeLessThanOrEqual(2),
);
},
});
Deno.test({
name: "toBeNull",
fn: async () => {
await assertAllPass(
() => expect(null).toBeNull(),
() => expect(undefined).not.toBeNull(),
() => expect(false).not.toBeNull(),
() => expect(Promise.resolve(null)).resolves.toBeNull(),
() => expect(Promise.reject(null)).rejects.toBeNull(),
);
await assertAllFail(
() => expect({}).toBeNull(),
() => expect(null).not.toBeNull(),
() => expect(undefined).toBeNull(),
);
},
});
Deno.test({
name: "toBeInstanceOf",
fn: async () => {
class A {}
class B {}
await assertAllPass(
() => expect(new A()).toBeInstanceOf(A),
() => expect(new A()).not.toBeInstanceOf(B),
);
await assertAllFail(
() => expect({}).toBeInstanceOf(A),
() => expect(null).toBeInstanceOf(A),
);
},
});
Deno.test({
name: "toBeNaN",
fn: async () => {
await assertAllPass(
() => expect(NaN).toBeNaN(),
() => expect(10).not.toBeNaN(),
() => expect(Promise.resolve(NaN)).resolves.toBeNaN(),
);
await assertAllFail(() => expect(10).toBeNaN(), () => expect(10).toBeNaN());
},
});
Deno.test({
name: "toBeMatch",
fn: async () => {
await assertAllPass(
() => expect("hello").toMatch(/^hell/),
() => expect("hello").toMatch("hello"),
() => expect("hello").toMatch("hell"),
);
await assertAllFail(() => expect("yo").toMatch(/^hell/));
},
});
Deno.test({
name: "toHaveProperty",
fn: async () => {
await assertAllPass(() => expect({ a: "10" }).toHaveProperty("a"));
await assertAllFail(() => expect({ a: 1 }).toHaveProperty("b"));
},
});
Deno.test({
name: "toHaveLength",
fn: async () => {
await assertAllPass(
() => expect([1, 2]).toHaveLength(2),
() => expect({ length: 10 }).toHaveLength(10),
);
await assertAllFail(() => expect([]).toHaveLength(10));
},
});
Deno.test({
name: "toContain",
fn: async () => {
await assertAllPass(
() => expect([1, 2, 3]).toContain(2),
() => expect([]).not.toContain(2),
);
await assertAllFail(
() => expect([1, 2, 3]).toContain(4),
() => expect([]).toContain(4),
);
},
});
Deno.test({
name: "toThrow",
fn: async () => {
await assertAllPass(
() =>
expect(() => {
throw new Error("TEST");
}).toThrow("TEST"),
() => expect(Promise.reject(new Error("TEST"))).rejects.toThrow("TEST"),
);
await assertAllFail(() => expect(() => true).toThrow());
},
});
Deno.test({
name: "toHaveBeenCalled",
fn: async () => {
await assertAllPass(
() => {
const m = mock.fn();
m(10);
m(20);
expect(m).toHaveBeenCalled();
},
() => {
const m = mock.fn();
expect(m).not.toHaveBeenCalled();
},
);
await assertAllFail(() => {
const m = mock.fn();
expect(m).toHaveBeenCalled();
});
},
});
Deno.test({
name: "toHaveBeenCalledTimes",
fn: async () => {
await assertAllPass(
() => {
const m = mock.fn();
expect(m).toHaveBeenCalledTimes(0);
},
() => {
const m = mock.fn();
m();
m();
expect(m).toHaveBeenCalledTimes(2);
},
);
await assertAllFail(
() => {
const m = mock.fn();
expect(m).toHaveBeenCalledTimes(1);
},
() => {
const m = mock.fn();
m();
m();
expect(m).toHaveBeenCalledTimes(3);
},
);
},
});
Deno.test({
name: "toHaveBeenCalledWith",
fn: async () => {
await assertAllPass(
() => {
const m = mock.fn();
m(1, 2, 3);
expect(m).toHaveBeenCalledWith(1, 2, 3);
},
() => {
const m = mock.fn();
m(1, 2, 3);
m(2, 3, 4);
expect(m).toHaveBeenCalledWith(1, 2, 3);
expect(m).toHaveBeenCalledWith(2, 3, 4);
},
);
await assertAllFail(
() => {
const m = mock.fn();
expect(m).toHaveBeenCalledWith(1);
},
() => {
const m = mock.fn();
m(2);
expect(m).toHaveBeenCalledWith(1);
},
);
},
});
Deno.test({
name: "toHaveBeenLastCalledWith",
fn: async () => {
await assertAllPass(
() => {
const m = mock.fn();
m(1, 2, 3);
expect(m).toHaveBeenLastCalledWith(1, 2, 3);
},
() => {
const m = mock.fn();
m(1, 2, 3);
m(2, 3, 4);
expect(m).not.toHaveBeenLastCalledWith(1, 2, 3);
expect(m).toHaveBeenLastCalledWith(2, 3, 4);
},
);
await assertAllFail(
() => {
const m = mock.fn();
expect(m).toHaveBeenLastCalledWith(1);
},
() => {
const m = mock.fn();
m(2);
expect(m).toHaveBeenLastCalledWith(1);
},
);
},
});
Deno.test({
name: "toHaveBeenNthCalledWith",
fn: async () => {
await assertAllPass(
() => {
const m = mock.fn();
m(1, 2, 3);
expect(m).toHaveBeenNthCalledWith(1, 1, 2, 3);
},
() => {
const m = mock.fn();
m(1, 2, 3);
m(2, 3, 4);
expect(m).not.toHaveBeenNthCalledWith(2, 1, 2, 3);
expect(m).toHaveBeenNthCalledWith(2, 2, 3, 4);
},
);
await assertAllFail(
() => {
const m = mock.fn();
expect(m).toHaveBeenNthCalledWith(1, 1, 2);
},
() => {
const m = mock.fn();
m(2);
expect(m).toHaveBeenNthCalledWith(1, 1);
},
);
},
});
Deno.test({
name: "toHaveReturnedWith",
fn: async () => {
await assertAllPass(
() => {
const m = mock.fn();
m();
expect(m).toHaveReturnedWith(undefined);
},
() => {
const m = mock.fn(() => true);
m();
expect(m).not.toHaveReturnedWith(false);
expect(m).toHaveReturnedWith(true);
},
() => {
const m = mock.fn(() => {
throw new Error("TEST");
});
try {
m();
} catch (err) {}
expect(m).not.toHaveReturnedWith(10);
},
);
await assertAllFail(
() => {
const m = mock.fn();
expect(m).toHaveReturnedWith(1);
},
() => {
const m = mock.fn();
m(2);
expect(m).toHaveReturnedWith(1);
},
);
},
});
Deno.test({
name: "toHaveReturnedTimes",
fn: async () => {
await assertAllPass(
() => {
const m = mock.fn();
m();
expect(m).toHaveReturnedTimes(1);
},
() => {
const m = mock.fn(() => true);
expect(m).toHaveReturnedTimes(0);
},
() => {
const m = mock.fn(() => {
throw new Error("TEST");
});
try {
m();
} catch (err) {}
expect(m).toHaveReturnedTimes(0);
},
);
await assertAllFail(
() => {
const m = mock.fn();
expect(m).toHaveReturnedTimes(1);
},
() => {
const m = mock.fn();
m(2);
expect(m).not.toHaveReturnedTimes(1);
},
);
},
});
Deno.test({
name: "toHaveReturned",
fn: async () => {
await assertAllPass(
() => {
const m = mock.fn();
m();
expect(m).toHaveReturned();
},
() => {
const m = mock.fn();
expect(m).not.toHaveReturned();
},
() => {
const m = mock.fn(() => {
throw new Error("TEST");
});
try {
m();
} catch (err) {}
expect(m).not.toHaveReturned();
},
);
await assertAllFail(
() => {
const m = mock.fn();
expect(m).toHaveReturned();
},
() => {
const m = mock.fn();
m();
expect(m).not.toHaveReturned();
},
() => {
const m = mock.fn(() => {
throw new Error("TEST");
});
m();
expect(m).not.toHaveReturned();
},
);
},
});
Deno.test({
name: "toHaveLastReturnedWith",
fn: async () => {
await assertAllPass(
() => {
const m = mock.fn((x: number) => x);
m(1);
m(2);
expect(m).toHaveLastReturnedWith(2);
},
() => {
const m = mock.fn((x: number) => x);
m(1);
m(2);
expect(m).toHaveLastReturnedWith(2);
},
);
await assertAllFail(
() => {
const m = mock.fn((x: number) => x);
expect(m).toHaveLastReturnedWith(1);
},
() => {
const m = mock.fn((x: number) => x);
m(2);
expect(m).toHaveLastReturnedWith(1);
},
);
},
});
Deno.test({
name: "toHaveNthReturnedWith",
fn: async () => {
await assertAllPass(
() => {
const m = mock.fn((x: number) => x);
m(1, 2, 3);
expect(m).toHaveNthReturnedWith(1, 1);
},
() => {
const m = mock.fn((x: number) => x);
m(1, 2, 3);
m(2, 3, 4);
expect(m).not.toHaveNthReturnedWith(2, 1);
expect(m).toHaveNthReturnedWith(2, 2);
},
);
await assertAllFail(
() => {
const m = mock.fn();
expect(m).toHaveNthReturnedWith(1, 1);
},
() => {
const m = mock.fn();
m(2);
expect(m).toHaveNthReturnedWith(1, 1);
},
);
},
});

606
expect/matchers.ts

@ -0,0 +1,606 @@
import {
bold,
green,
red,
white,
} from "https://deno.land/std@0.78.0/fmt/colors.ts";
import { equal } from "https://deno.land/std@0.78.0/testing/asserts.ts";
import {
diff,
DiffResult,
DiffType,
} from "https://deno.land/std@0.78.0/testing/_diff.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 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): MatchResult {
if (typeof value === "object" && typeof value[propName] !== "undefined") {
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)}`,
);
}
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 (Array.isArray(value) && value.includes(item)) return { pass: true };
const actualString = createStr(value);
const itemString = createStr(item);
if (Array.isArray(value)) {
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 contain ${green(itemString)} but it is ${red("not")} an array`,
);
}
}
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 messages
export 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 };
}

717
expect/matchers_test.ts

@ -0,0 +1,717 @@
import {
assert,
assertEquals,
} from "https://deno.land/std@0.78.0/testing/asserts.ts";
import {
MatchResult,
toBe,
toBeDefined,
toBeFalsy,
toBeGreaterThan,
toBeInstanceOf,
toBeLessThan,
toBeLessThanOrEqual,
toBeNaN,
toBeNull,
toBeTruthy,
toBeUndefined,
toContain,
toEqual,
toHaveBeenCalled,
toHaveBeenCalledTimes,
toHaveBeenCalledWith,
toHaveBeenLastCalledWith,
toHaveBeenNthCalledWith,
toHaveLastReturnedWith,
toHaveLength,
toHaveNthReturnedWith,
toHaveProperty,
toHaveReturned,
toHaveReturnedTimes,
toHaveReturnedWith,
toMatch,
toThrow,
} from "./matchers.ts";
import * as mock from "./mock.ts";
function assertResult(actual: MatchResult, expected: MatchResult) {
assertEquals(
actual.pass,
expected.pass,
`expected to be ${
expected.pass ? `pass but received: ${actual.message}` : "fail"
}`,
);
if (typeof expected.message !== "undefined") {
assert(!!actual.message, "no message given");
const colourless = actual.message.replace(
/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g,
"",
);
const trim = (x: string) => x.trim().replace(/\s*\n\s+/g, "\n");
assertEquals(trim(colourless), trim(expected.message));
}
}
function assertResultPass(result: any) {
assertResult(result, { pass: true });
}
Deno.test({
name: "toBePass",
fn: () => {
assertResultPass(toBe(10, 10));
},
});
Deno.test({
name: "toBeFail",
fn: () => {
assertResult(toBe(10, 20), {
pass: false,
message: `expect(actual).toBe(expected)
- 10
+ 20`,
});
assertResult(toBe({}, {}), {
pass: false,
message: `expect(actual).toBe(expected)
{}`,
});
},
});
Deno.test({
name: "toEqualPass",
fn: () => {
assertResultPass(toEqual({ a: 1 }, { a: 1 }));
assertResultPass(toEqual(1, 1));
assertResultPass(toEqual([1], [1]));
},
});
Deno.test({
name: "toEqualFail",
fn: () => {
assertResult(toEqual(10, 20), {
pass: false,
message: `expect(actual).toEqual(expected)\n\n- 10\n+ 20`,
});
assertResult(toEqual({ a: 1 }, { a: 2 }), {
pass: false,
message: `expect(actual).toEqual(expected)
- { a: 1 }
+ { a: 2 }`,
});
},
});
Deno.test({
name: "toBeGreaterThanPass",
fn: () => {
assertResultPass(toBeGreaterThan(2, 1));
},
});
Deno.test({