Skip to content

Commit

Permalink
feat(test): toHaveBeenNthCalledWith + improve some fail messages (#…
Browse files Browse the repository at this point in the history
…7320)

* feat(test): `toHaveBeenNthCalledWith` + improve some fail messages

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
  • Loading branch information
james-elicx and autofix-ci[bot] authored Nov 26, 2023
1 parent 6aa1319 commit 1840de3
Show file tree
Hide file tree
Showing 5 changed files with 115 additions and 40 deletions.
2 changes: 1 addition & 1 deletion docs/test/writing.md
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,7 @@ Bun implements the following matchers. Full Jest compatibility is on the roadmap

---

-
-
- [`.toHaveBeenNthCalledWith()`](https://jestjs.io/docs/expect#tohavebeennthcalledwithnthcall-arg1-arg2-)

---
Expand Down
4 changes: 4 additions & 0 deletions packages/bun-types/bun-test.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1149,6 +1149,10 @@ declare module "bun:test" {
* Ensure that a mock function is called with specific arguments for the last call.
*/
toHaveBeenLastCalledWith(...expected: Array<unknown>): void;
/**
* Ensure that a mock function is called with specific arguments for the nth call.
*/
toHaveBeenNthCalledWith(n: number, ...expected: Array<unknown>): void;
};
}

Expand Down
123 changes: 86 additions & 37 deletions src/bun.js/test/expect.zig
Original file line number Diff line number Diff line change
Expand Up @@ -3348,29 +3348,19 @@ pub const Expect = struct {
if (pass) return thisValue;

// handle failure
var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true };
if (not) {
const signature = comptime getSignature("toHaveBeenCalled", "", true);
const fmt = signature ++ "\n\nExpected: not <green>{any}<r>\n";
if (Output.enable_ansi_colors) {
globalObject.throw(Output.prettyFmt(fmt, true), .{calls.toFmt(globalObject, &formatter)});
return .zero;
}
globalObject.throw(Output.prettyFmt(fmt, false), .{calls.toFmt(globalObject, &formatter)});
return .zero;
} else {
const signature = comptime getSignature("toHaveBeenCalled", "", false);
const fmt = signature ++ "\n\nExpected <green>{any}<r>\n";
if (Output.enable_ansi_colors) {
globalObject.throw(Output.prettyFmt(fmt, true), .{calls.toFmt(globalObject, &formatter)});
return .zero;
}
globalObject.throw(Output.prettyFmt(fmt, false), .{calls.toFmt(globalObject, &formatter)});
const fmt = signature ++ "\n\n" ++ "Expected number of calls: <green>0<r>\n" ++ "Received number of calls: <red>{any}<r>\n";
globalObject.throwPretty(fmt, .{calls.getLength(globalObject)});
return .zero;
}

unreachable;
const signature = comptime getSignature("toHaveBeenCalled", "", false);
const fmt = signature ++ "\n\n" ++ "Expected number of calls: \\>= <green>1<r>\n" ++ "Received number of calls: <red>{any}<r>\n";
globalObject.throwPretty(fmt, .{calls.getLength(globalObject)});
return .zero;
}

pub fn toHaveBeenCalledTimes(this: *Expect, globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSC.JSValue {
JSC.markBinding(@src());

Expand All @@ -3389,8 +3379,8 @@ pub const Expect = struct {
return .zero;
}

if (arguments.len < 1 or !arguments[0].isAnyInt()) {
globalObject.throwInvalidArguments("toHaveBeenCalledTimes() requires 1 integer argument", .{});
if (arguments.len < 1 or !arguments[0].isUInt32AsAnyInt()) {
globalObject.throwInvalidArguments("toHaveBeenCalledTimes() requires 1 non-negative integer argument", .{});
return .zero;
}

Expand All @@ -3403,28 +3393,17 @@ pub const Expect = struct {
if (pass) return thisValue;

// handle failure
var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true };
if (not) {
const signature = comptime getSignature("toHaveBeenCalledTimes", "<green>expected<r>", true);
const fmt = signature ++ "\n\nExpected: not <green>{any}<r>\n";
if (Output.enable_ansi_colors) {
globalObject.throw(Output.prettyFmt(fmt, true), .{calls.toFmt(globalObject, &formatter)});
return .zero;
}
globalObject.throw(Output.prettyFmt(fmt, false), .{calls.toFmt(globalObject, &formatter)});
return .zero;
} else {
const signature = comptime getSignature("toHaveBeenCalledTimes", "<green>expected<r>", false);
const fmt = signature ++ "\n\nExpected <green>{any}<r>\n";
if (Output.enable_ansi_colors) {
globalObject.throw(Output.prettyFmt(fmt, true), .{calls.toFmt(globalObject, &formatter)});
return .zero;
}
globalObject.throw(Output.prettyFmt(fmt, false), .{calls.toFmt(globalObject, &formatter)});
const fmt = signature ++ "\n\n" ++ "Expected number of calls: not <green>{any}<r>\n" ++ "Received number of calls: <red>{any}<r>\n";
globalObject.throwPretty(fmt, .{ times, calls.getLength(globalObject) });
return .zero;
}

unreachable;
const signature = comptime getSignature("toHaveBeenCalledTimes", "<green>expected<r>", false);
const fmt = signature ++ "\n\n" ++ "Expected number of calls: <green>{any}<r>\n" ++ "Received number of calls: <red>{any}<r>\n";
globalObject.throwPretty(fmt, .{ times, calls.getLength(globalObject) });
return .zero;
}

pub fn toMatchObject(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSValue {
Expand Down Expand Up @@ -3624,7 +3603,77 @@ pub const Expect = struct {
return .zero;
}

pub const toHaveBeenNthCalledWith = notImplementedJSCFn;
pub fn toHaveBeenNthCalledWith(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, "toHaveBeenNthCalledWith", "<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 nthCallNum = if (arguments.len > 0 and arguments[0].isUInt32AsAnyInt()) arguments[0].coerce(i32, globalObject) else 0;
if (nthCallNum < 1) {
globalObject.throwInvalidArguments("toHaveBeenNthCalledWith() requires a positive integer argument", .{});
return .zero;
}

const totalCalls = calls.getLength(globalObject);
var nthCallValue: JSValue = .zero;

var pass = totalCalls >= nthCallNum;

if (pass) {
nthCallValue = calls.getIndex(globalObject, @as(u32, @intCast(nthCallNum)) - 1);

if (nthCallValue == .zero or !nthCallValue.jsType().isArray()) {
globalObject.throw("Expected value must be a mock function with calls: {}", .{value});
return .zero;
}

if (nthCallValue.getLength(globalObject) != (arguments.len - 1)) {
pass = false;
} else {
var itr = nthCallValue.arrayIterator(globalObject);
while (itr.next()) |callArg| {
if (!callArg.jestDeepEquals(arguments[itr.i], 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 = nthCallValue.toFmt(globalObject, &formatter);

if (not) {
const signature = comptime getSignature("toHaveBeenNthCalledWith", "<green>expected<r>", true);
const fmt = signature ++ "\n\n" ++ "n: {any}\n" ++ "Received: <red>{any}<r>" ++ "\n\n" ++ "Number of calls: <red>{any}<r>\n";
globalObject.throwPretty(fmt, .{ nthCallNum, received_fmt, totalCalls });
return .zero;
}

const signature = comptime getSignature("toHaveBeenNthCalledWith", "<green>expected<r>", false);
const fmt = signature ++ "\n\n" ++ "n: {any}\n" ++ "Received: <red>{any}<r>" ++ "\n\n" ++ "Number of calls: <red>{any}<r>\n";
globalObject.throwPretty(fmt, .{ nthCallNum, received_fmt, totalCalls });
return .zero;
}

pub const toHaveReturnedTimes = notImplementedJSCFn;
pub const toHaveReturnedWith = notImplementedJSCFn;
pub const toHaveLastReturnedWith = notImplementedJSCFn;
Expand Down
1 change: 0 additions & 1 deletion src/bun.js/test/jest.classes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,6 @@ export default [
},
toHaveBeenNthCalledWith: {
fn: "toHaveBeenNthCalledWith",
length: 1,
},
toHaveReturnedTimes: {
fn: "toHaveReturnedTimes",
Expand Down
25 changes: 24 additions & 1 deletion test/js/bun/test/mock-fn.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -563,36 +563,59 @@ describe("mock()", () => {
test("toHaveBeenCalledWith, toHaveBeenLastCalledWith works", () => {
const fn = jest.fn();
expect(() => expect(() => {}).not.toHaveBeenLastCalledWith()).toThrow();
expect(() => expect(() => {}).not.toHaveBeenNthCalledWith()).toThrow();
expect(() => expect(() => {}).not.toHaveBeenCalledWith()).toThrow();
expect(fn).not.toHaveBeenCalled();
expect(() => expect(fn).toHaveBeenCalledTimes(-1)).toThrow();
expect(fn).toHaveBeenCalledTimes(0);
expect(fn).not.toHaveBeenCalledWith();
expect(fn).not.toHaveBeenLastCalledWith();
expect(() => expect(fn).toHaveBeenNthCalledWith(0)).toThrow();
expect(() => expect(fn).toHaveBeenNthCalledWith(-1)).toThrow();
expect(() => expect(fn).toHaveBeenNthCalledWith(1.1)).toThrow();
expect(fn).not.toHaveBeenNthCalledWith(1);
fn();
expect(fn).toHaveBeenCalled();
expect(fn).toHaveBeenCalledTimes(1);
expect(fn).toHaveBeenCalledWith();
expect(fn).toHaveBeenLastCalledWith();
expect(fn).toHaveBeenNthCalledWith(1);
expect(fn).not.toHaveBeenNthCalledWith(1, 1);
expect(fn).not.toHaveBeenCalledWith(1);
fn(1);
expect(fn).toHaveBeenCalledWith(1);
expect(fn).toHaveBeenLastCalledWith(1);
expect(fn).toHaveBeenNthCalledWith(1);
expect(fn).toHaveBeenNthCalledWith(2, 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);
expect(fn).toHaveBeenNthCalledWith(3, 1, 2, 3);
expect(fn).not.toHaveBeenNthCalledWith(4, 3, 2, 1);
fn("random string");
expect(fn).toHaveBeenCalledWith();
expect(fn).toHaveBeenNthCalledWith(1);
expect(fn).toHaveBeenCalledWith(1);
expect(fn).toHaveBeenNthCalledWith(2, 1);
expect(fn).toHaveBeenCalledWith(1, 2, 3);
expect(fn).toHaveBeenNthCalledWith(3, 1, 2, 3);
expect(fn).toHaveBeenCalledWith("random string");
expect(fn).toHaveBeenLastCalledWith("random string");
expect(fn).toHaveBeenNthCalledWith(4, "random string");
expect(fn).toHaveBeenCalledWith(expect.stringMatching(/^random \w+$/));
expect(fn).toHaveBeenLastCalledWith(expect.stringMatching(/^random \w+$/));
expect(fn).toHaveBeenNthCalledWith(4, expect.stringMatching(/^random \w+$/));
fn(1, undefined);
expect(fn).not.toHaveBeenLastCalledWith(1);
expect(fn).toHaveBeenLastCalledWith(1, undefined);
expect(fn).not.toHaveBeenLastCalledWith(1);
expect(fn).toHaveBeenCalledWith(1, undefined);
expect(fn).not.toHaveBeenCalledWith(undefined);
expect(fn).toHaveBeenNthCalledWith(5, 1, undefined);
expect(fn).not.toHaveBeenNthCalledWith(5, 1);
});
});

Expand Down

0 comments on commit 1840de3

Please sign in to comment.