From b46f5b507826523c035a606390f49bb3e6343432 Mon Sep 17 00:00:00 2001 From: James Anderson Date: Sun, 26 Nov 2023 23:59:44 +0000 Subject: [PATCH] feat(test): `toHaveBeenNthCalledWith` + improve some fail messages (#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> --- docs/test/writing.md | 2 +- packages/bun-types/bun-test.d.ts | 4 + src/bun.js/test/expect.zig | 123 +++++++++++++++++++++---------- src/bun.js/test/jest.classes.ts | 1 - test/js/bun/test/mock-fn.test.js | 25 ++++++- 5 files changed, 115 insertions(+), 40 deletions(-) diff --git a/docs/test/writing.md b/docs/test/writing.md index b833af3c736a4d..32eb463c084fbe 100644 --- a/docs/test/writing.md +++ b/docs/test/writing.md @@ -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-) --- diff --git a/packages/bun-types/bun-test.d.ts b/packages/bun-types/bun-test.d.ts index 90e56e8a0df076..f92049f0b5183a 100644 --- a/packages/bun-types/bun-test.d.ts +++ b/packages/bun-types/bun-test.d.ts @@ -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): void; + /** + * Ensure that a mock function is called with specific arguments for the nth call. + */ + toHaveBeenNthCalledWith(n: number, ...expected: Array): void; }; } diff --git a/src/bun.js/test/expect.zig b/src/bun.js/test/expect.zig index b8c7626035a36b..2a43cb9eaa2c41 100644 --- a/src/bun.js/test/expect.zig +++ b/src/bun.js/test/expect.zig @@ -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 {any}\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 {any}\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: 0\n" ++ "Received number of calls: {any}\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: \\>= 1\n" ++ "Received number of calls: {any}\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()); @@ -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; } @@ -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", "expected", true); - const fmt = signature ++ "\n\nExpected: not {any}\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", "expected", false); - const fmt = signature ++ "\n\nExpected {any}\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 {any}\n" ++ "Received number of calls: {any}\n"; + globalObject.throwPretty(fmt, .{ times, calls.getLength(globalObject) }); return .zero; } - unreachable; + const signature = comptime getSignature("toHaveBeenCalledTimes", "expected", false); + const fmt = signature ++ "\n\n" ++ "Expected number of calls: {any}\n" ++ "Received number of calls: {any}\n"; + globalObject.throwPretty(fmt, .{ times, calls.getLength(globalObject) }); + return .zero; } pub fn toMatchObject(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSValue { @@ -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", "expected") 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", "expected", true); + const fmt = signature ++ "\n\n" ++ "n: {any}\n" ++ "Received: {any}" ++ "\n\n" ++ "Number of calls: {any}\n"; + globalObject.throwPretty(fmt, .{ nthCallNum, received_fmt, totalCalls }); + return .zero; + } + + const signature = comptime getSignature("toHaveBeenNthCalledWith", "expected", false); + const fmt = signature ++ "\n\n" ++ "n: {any}\n" ++ "Received: {any}" ++ "\n\n" ++ "Number of calls: {any}\n"; + globalObject.throwPretty(fmt, .{ nthCallNum, received_fmt, totalCalls }); + return .zero; + } + pub const toHaveReturnedTimes = notImplementedJSCFn; pub const toHaveReturnedWith = notImplementedJSCFn; pub const toHaveLastReturnedWith = notImplementedJSCFn; diff --git a/src/bun.js/test/jest.classes.ts b/src/bun.js/test/jest.classes.ts index 8100c9a7164fbd..5f421918aba6ed 100644 --- a/src/bun.js/test/jest.classes.ts +++ b/src/bun.js/test/jest.classes.ts @@ -152,7 +152,6 @@ export default [ }, toHaveBeenNthCalledWith: { fn: "toHaveBeenNthCalledWith", - length: 1, }, toHaveReturnedTimes: { fn: "toHaveReturnedTimes", diff --git a/test/js/bun/test/mock-fn.test.js b/test/js/bun/test/mock-fn.test.js index 79e9f53249f10f..7844889f88640b 100644 --- a/test/js/bun/test/mock-fn.test.js +++ b/test/js/bun/test/mock-fn.test.js @@ -563,16 +563,30 @@ 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); @@ -580,19 +594,28 @@ describe("mock()", () => { 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); }); });