feat(test): toHaveBeenCalledWith and toHaveBeenLastCalledWith (#7277)

This commit is contained in:
James Anderson
2023-11-25 02:24:21 +00:00
committed by GitHub
parent 5692f82aaf
commit 5f86b839b4
6 changed files with 223 additions and 8 deletions

View File

@@ -32,7 +32,6 @@ Some notable missing features:
- `expect.extend()`
- `expect().toMatchInlineSnapshot()`
- `expect().toHaveBeenCalledWith()`
- `expect().toHaveReturned()`
---

View File

@@ -313,12 +313,12 @@ Bun implements the following matchers. Full Jest compatibility is on the roadmap
---
-
-
- [`.toHaveBeenCalledWith()`](https://jestjs.io/docs/expect#tohavebeencalledwitharg1-arg2-)
---
-
-
- [`.toHaveBeenLastCalledWith()`](https://jestjs.io/docs/expect#tohavebeenlastcalledwitharg1-arg2-)
---

View File

@@ -1144,7 +1144,11 @@ declare module "bun:test" {
/**
* Ensure that a mock function is called with specific arguments.
*/
// toHaveBeenCalledWith(...expected: Array<unknown>): void;
toHaveBeenCalledWith(...expected: Array<unknown>): void;
/**
* Ensure that a mock function is called with specific arguments for the last call.
*/
toHaveBeenLastCalledWith(...expected: Array<unknown>): void;
};
}

View File

@@ -3493,8 +3493,137 @@ pub const Expect = struct {
return .zero;
}
pub const toHaveBeenCalledWith = notImplementedJSCFn;
pub const toHaveBeenLastCalledWith = notImplementedJSCFn;
pub fn toHaveBeenCalledWith(this: *Expect, globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSC.JSValue {
JSC.markBinding(@src());
const thisValue = callframe.this();
const arguments_ = callframe.argumentsPtr()[0..callframe.argumentsCount()];
const arguments: []const JSValue = arguments_.ptr[0..arguments_.len];
defer this.postMatch(globalObject);
const value: JSValue = this.getValue(globalObject, thisValue, "toHaveBeenCalledWith", "<green>expected<r>") orelse return .zero;
active_test_expectation_counter.actual += 1;
const calls = JSMockFunction__getCalls(value);
if (calls == .zero or !calls.jsType().isArray()) {
globalObject.throw("Expected value must be a mock function: {}", .{value});
return .zero;
}
var pass = false;
if (calls.getLength(globalObject) > 0) {
var itr = calls.arrayIterator(globalObject);
while (itr.next()) |callItem| {
if (callItem == .zero or !callItem.jsType().isArray()) {
globalObject.throw("Expected value must be a mock function with calls: {}", .{value});
return .zero;
}
if (callItem.getLength(globalObject) != arguments.len) {
continue;
}
var callItr = callItem.arrayIterator(globalObject);
var match = true;
while (callItr.next()) |callArg| {
if (!callArg.jestDeepEquals(arguments[callItr.i - 1], globalObject)) {
match = false;
break;
}
}
if (match) {
pass = true;
break;
}
}
}
const not = this.flags.not;
if (not) pass = !pass;
if (pass) return thisValue;
// handle failure
if (not) {
const signature = comptime getSignature("toHaveBeenCalledWith", "<green>expected<r>", true);
const fmt = signature ++ "\n\n" ++ "Number of calls: <red>{any}<r>\n";
globalObject.throwPretty(fmt, .{calls.getLength(globalObject)});
return .zero;
}
const signature = comptime getSignature("toHaveBeenCalledWith", "<green>expected<r>", false);
const fmt = signature ++ "\n\n" ++ "Number of calls: <red>{any}<r>\n";
globalObject.throwPretty(fmt, .{calls.getLength(globalObject)});
return .zero;
}
pub fn toHaveBeenLastCalledWith(this: *Expect, globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSC.JSValue {
JSC.markBinding(@src());
const thisValue = callframe.this();
const arguments_ = callframe.argumentsPtr()[0..callframe.argumentsCount()];
const arguments: []const JSValue = arguments_.ptr[0..arguments_.len];
defer this.postMatch(globalObject);
const value: JSValue = this.getValue(globalObject, thisValue, "toHaveBeenLastCalledWith", "<green>expected<r>") orelse return .zero;
active_test_expectation_counter.actual += 1;
const calls = JSMockFunction__getCalls(value);
if (calls == .zero or !calls.jsType().isArray()) {
globalObject.throw("Expected value must be a mock function: {}", .{value});
return .zero;
}
const totalCalls = @as(u32, @intCast(calls.getLength(globalObject)));
var lastCallValue: JSValue = .zero;
var pass = totalCalls > 0;
if (pass) {
lastCallValue = calls.getIndex(globalObject, totalCalls - 1);
if (lastCallValue == .zero or !lastCallValue.jsType().isArray()) {
globalObject.throw("Expected value must be a mock function with calls: {}", .{value});
return .zero;
}
if (lastCallValue.getLength(globalObject) != arguments.len) {
pass = false;
} else {
var itr = lastCallValue.arrayIterator(globalObject);
while (itr.next()) |callArg| {
if (!callArg.jestDeepEquals(arguments[itr.i - 1], globalObject)) {
pass = false;
break;
}
}
}
}
const not = this.flags.not;
if (not) pass = !pass;
if (pass) return thisValue;
// handle failure
var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true };
const received_fmt = lastCallValue.toFmt(globalObject, &formatter);
if (not) {
const signature = comptime getSignature("toHaveBeenLastCalledWith", "<green>expected<r>", true);
const fmt = signature ++ "\n\n" ++ "Received: <red>{any}<r>" ++ "\n\n" ++ "Number of calls: <red>{any}<r>\n";
globalObject.throwPretty(fmt, .{ received_fmt, totalCalls });
return .zero;
}
const signature = comptime getSignature("toHaveBeenLastCalledWith", "<green>expected<r>", false);
const fmt = signature ++ "\n\n" ++ "Received: <red>{any}<r>" ++ "\n\n" ++ "Number of calls: <red>{any}<r>\n";
globalObject.throwPretty(fmt, .{ received_fmt, totalCalls });
return .zero;
}
pub const toHaveBeenNthCalledWith = notImplementedJSCFn;
pub const toHaveReturnedTimes = notImplementedJSCFn;
pub const toHaveReturnedWith = notImplementedJSCFn;

View File

@@ -146,11 +146,9 @@ export default [
},
toHaveBeenCalledWith: {
fn: "toHaveBeenCalledWith",
length: 1,
},
toHaveBeenLastCalledWith: {
fn: "toHaveBeenLastCalledWith",
length: 1,
},
toHaveBeenNthCalledWith: {
fn: "toHaveBeenNthCalledWith",

View File

@@ -65,10 +65,13 @@ describe("mock()", () => {
expect(fn).toHaveBeenCalledTimes(1);
expect(fn.mock.calls).toHaveLength(1);
expect(fn.mock.calls[0]).toBeEmpty();
expect(fn).toHaveBeenLastCalledWith();
expect(fn()).toBe(42);
expect(fn).toHaveBeenCalledTimes(2);
expect(fn.mock.calls).toHaveLength(2);
expect(fn.mock.calls[1]).toBeEmpty();
expect(fn).toHaveBeenLastCalledWith();
expect(fn).toHaveBeenCalledWith();
});
test("passes this value", () => {
const fn = jest.fn(function hey() {
@@ -107,10 +110,13 @@ describe("mock()", () => {
expect(fn).toHaveBeenCalledTimes(1);
expect(fn.mock.calls).toHaveLength(1);
expect(fn.mock.calls[0]).toBeEmpty();
expect(fn).toHaveBeenLastCalledWith();
expect(Number(fn.call(234))).toBe(234);
expect(fn).toHaveBeenCalledTimes(2);
expect(fn.mock.calls).toHaveLength(2);
expect(fn.mock.calls[1]).toBeEmpty();
expect(fn).toHaveBeenLastCalledWith();
expect(fn).toHaveBeenCalledWith();
});
test(".apply works", function () {
const fn = jest.fn(function hey() {
@@ -121,10 +127,13 @@ describe("mock()", () => {
expect(fn).toHaveBeenCalledTimes(1);
expect(fn.mock.calls).toHaveLength(1);
expect(fn.mock.calls[0]).toBeEmpty();
expect(fn).toHaveBeenLastCalledWith();
expect(Number(fn.apply(234))).toBe(234);
expect(fn).toHaveBeenCalledTimes(2);
expect(fn.mock.calls).toHaveLength(2);
expect(fn.mock.calls[1]).toBeEmpty();
expect(fn).toHaveBeenLastCalledWith();
expect(fn).toHaveBeenCalledWith();
});
test(".bind works", () => {
const fn = jest.fn(function hey() {
@@ -135,10 +144,13 @@ describe("mock()", () => {
expect(fn).toHaveBeenCalledTimes(1);
expect(fn.mock.calls).toHaveLength(1);
expect(fn.mock.calls[0]).toBeEmpty();
expect(fn).toHaveBeenLastCalledWith();
expect(Number(fn.bind(234)())).toBe(234);
expect(fn).toHaveBeenCalledTimes(2);
expect(fn.mock.calls).toHaveLength(2);
expect(fn.mock.calls[1]).toBeEmpty();
expect(fn).toHaveBeenLastCalledWith();
expect(fn).toHaveBeenCalledWith();
});
test(".name works", () => {
const fn = jest.fn(function hey() {
@@ -181,6 +193,10 @@ describe("mock()", () => {
value: 43,
});
expect(fn.mock.calls[0]).toEqual([43]);
expect(fn).toHaveBeenCalled();
expect(fn).toHaveBeenCalledTimes(1);
expect(fn).toHaveBeenLastCalledWith(43);
expect(fn).toHaveBeenCalledWith(43);
});
test("works when throwing", () => {
const instance = new Error("foo");
@@ -193,6 +209,8 @@ describe("mock()", () => {
value: instance,
});
expect(fn.mock.calls[0]).toEqual([43]);
expect(fn).toHaveBeenLastCalledWith(43);
expect(fn).toHaveBeenCalledWith(43);
});
test("mockReset works", () => {
const instance = new Error("foo");
@@ -205,11 +223,15 @@ describe("mock()", () => {
value: instance,
});
expect(fn.mock.calls[0]).toEqual([43]);
expect(fn).toHaveBeenLastCalledWith(43);
expect(fn).toHaveBeenCalledWith(43);
fn.mockReset();
expect(fn.mock.calls).toBeEmpty();
expect(fn.mock.results).toBeEmpty();
expect(fn.mock.instances).toBeEmpty();
expect(fn).not.toHaveBeenCalled();
expect(fn).not.toHaveBeenLastCalledWith(43);
expect(fn).not.toHaveBeenCalledWith(43);
expect(() => expect(fn).toHaveBeenCalled()).toThrow();
expect(fn(43)).toBe(undefined);
expect(fn.mock.results).toEqual([
@@ -219,6 +241,8 @@ describe("mock()", () => {
},
]);
expect(fn.mock.calls).toEqual([[43]]);
expect(fn).toHaveBeenLastCalledWith(43);
expect(fn).toHaveBeenCalledWith(43);
});
test("mockClear works", () => {
const instance = new Error("foo");
@@ -231,17 +255,23 @@ describe("mock()", () => {
value: instance,
});
expect(fn.mock.calls[0]).toEqual([43]);
expect(fn).toHaveBeenLastCalledWith(43);
expect(fn).toHaveBeenCalledWith(43);
fn.mockClear();
expect(fn.mock.calls).toBeEmpty();
expect(fn.mock.results).toBeEmpty();
expect(fn.mock.instances).toBeEmpty();
expect(fn).not.toHaveBeenCalled();
expect(fn).not.toHaveBeenLastCalledWith(43);
expect(fn).not.toHaveBeenCalledWith(43);
expect(() => fn(43)).toThrow("foo");
expect(fn.mock.results[0]).toEqual({
type: "throw",
value: instance,
});
expect(fn.mock.calls[0]).toEqual([43]);
expect(fn).toHaveBeenLastCalledWith(43);
expect(fn).toHaveBeenCalledWith(43);
});
// this is an implementation detail i don't think we *need* to support
test("mockClear doesnt update existing object", () => {
@@ -255,10 +285,14 @@ describe("mock()", () => {
value: instance,
});
expect(fn.mock.calls[0]).toEqual([43]);
expect(fn).toHaveBeenLastCalledWith(43);
expect(fn).toHaveBeenCalledWith(43);
const stolen = fn.mock;
fn.mockClear();
expect(stolen).not.toBe(fn.mock);
expect(fn.mock.calls).toBeEmpty();
expect(fn).not.toHaveBeenLastCalledWith(43);
expect(fn).not.toHaveBeenCalledWith(43);
expect(stolen.calls).not.toBeEmpty();
expect(fn.mock.results).toBeEmpty();
expect(stolen.results).not.toBeEmpty();
@@ -271,22 +305,29 @@ describe("mock()", () => {
value: instance,
});
expect(fn.mock.calls[0]).toEqual([43]);
expect(fn).toHaveBeenLastCalledWith(43);
expect(fn).toHaveBeenCalledWith(43);
});
test("multiple calls work", () => {
const fn = jest.fn(f => f);
expect(fn(43)).toBe(43);
expect(fn).toHaveBeenLastCalledWith(43);
expect(fn(44)).toBe(44);
expect(fn).toHaveBeenLastCalledWith(44);
expect(fn.mock.calls[0]).toEqual([43]);
expect(fn.mock.results[0]).toEqual({
type: "return",
value: 43,
});
expect(fn.mock.calls[1]).toEqual([44]);
expect(fn).toHaveBeenLastCalledWith(44);
expect(fn.mock.results[1]).toEqual({
type: "return",
value: 44,
});
expect(fn.mock.contexts).toEqual([undefined, undefined]);
expect(fn).toHaveBeenCalledWith(43);
expect(fn).toHaveBeenCalledWith(44);
});
test("this arg", () => {
const fn = jest.fn(function (add) {
@@ -295,6 +336,8 @@ describe("mock()", () => {
const obj = { foo: 42, fn };
expect(obj.fn(2)).toBe(44);
expect(fn.mock.calls[0]).toEqual([2]);
expect(fn).toHaveBeenLastCalledWith(2);
expect(fn).toHaveBeenCalledWith(2);
expect(fn.mock.results[0]).toEqual({
type: "return",
value: 44,
@@ -320,19 +363,26 @@ describe("mock()", () => {
});
const obj = { foo: 42, fn };
expect(obj.fn(2)).toBe(44);
expect(fn).toHaveBeenLastCalledWith(2);
const this2 = { foo: 43 };
expect(fn.call(this2, 2)).toBe(45);
expect(fn).toHaveBeenLastCalledWith(2);
const this3 = { foo: 44 };
expect(fn.apply(this3, [2])).toBe(46);
expect(fn).toHaveBeenLastCalledWith(2);
const this4 = { foo: 45 };
expect(fn.bind(this4)(3)).toBe(48);
expect(fn).toHaveBeenLastCalledWith(3);
const this5 = { foo: 45 };
expect(fn.bind(this5, 2)()).toBe(47);
expect(fn).toHaveBeenLastCalledWith(2);
expect(fn.mock.calls[0]).toEqual([2]);
expect(fn.mock.calls[1]).toEqual([2]);
expect(fn.mock.calls[2]).toEqual([2]);
expect(fn.mock.calls[3]).toEqual([3]);
expect(fn.mock.calls[4]).toEqual([2]);
expect(fn).toHaveBeenCalledWith(2);
expect(fn).toHaveBeenCalledWith(3);
expect(fn.mock.results[0]).toEqual({
type: "return",
value: 44,
@@ -509,6 +559,41 @@ describe("mock()", () => {
expect(fn1.mock.invocationCallOrder).toEqual([first, first + 3]);
expect(fn2.mock.invocationCallOrder).toEqual([first + 1, first + 2]);
});
test("toHaveBeenCalledWith, toHaveBeenLastCalledWith works", () => {
const fn = jest.fn();
expect(() => expect(() => {}).not.toHaveBeenLastCalledWith()).toThrow();
expect(() => expect(() => {}).not.toHaveBeenCalledWith()).toThrow();
expect(fn).not.toHaveBeenCalledWith();
expect(fn).not.toHaveBeenLastCalledWith();
fn();
expect(fn).toHaveBeenCalledWith();
expect(fn).toHaveBeenLastCalledWith();
expect(fn).not.toHaveBeenCalledWith(1);
fn(1);
expect(fn).toHaveBeenCalledWith(1);
expect(fn).toHaveBeenLastCalledWith(1);
fn(1, 2, 3);
expect(fn).not.toHaveBeenCalledWith("123");
expect(fn).not.toHaveBeenLastCalledWith(1);
expect(fn).not.toHaveBeenLastCalledWith(1, 2);
expect(fn).not.toHaveBeenLastCalledWith("123");
expect(fn).toHaveBeenLastCalledWith(1, 2, 3);
expect(fn).not.toHaveBeenLastCalledWith(3, 2, 1);
fn("random string");
expect(fn).toHaveBeenCalledWith();
expect(fn).toHaveBeenCalledWith(1);
expect(fn).toHaveBeenCalledWith(1, 2, 3);
expect(fn).toHaveBeenCalledWith("random string");
expect(fn).toHaveBeenLastCalledWith("random string");
expect(fn).toHaveBeenCalledWith(expect.stringMatching(/^random \w+$/));
expect(fn).toHaveBeenLastCalledWith(expect.stringMatching(/^random \w+$/));
fn(1, undefined);
expect(fn).not.toHaveBeenLastCalledWith(1);
expect(fn).toHaveBeenLastCalledWith(1, undefined);
expect(fn).toHaveBeenCalledWith(1, undefined);
expect(fn).not.toHaveBeenCalledWith(undefined);
});
});
describe("spyOn", () => {