Implement expect().toHaveReturned() && expect().toHaveReturnedTimes(n) (#11231)

This commit is contained in:
Jarred Sumner
2024-05-21 13:11:26 -07:00
committed by GitHub
parent 396dc78522
commit bbaeeaeed2
3 changed files with 146 additions and 2 deletions

View File

@@ -1444,6 +1444,22 @@ declare module "bun:test" {
* @param expected the string to end with
*/
toEndWith(expected: string): void;
/**
* Ensures that a mock function has returned successfully at least once.
*
* A promise that is unfulfilled will be considered a failure. If the
* function threw an error, it will be considered a failure.
*/
toHaveReturned(): void;
/**
* Ensures that a mock function has returned successfully at `times` times.
*
* A promise that is unfulfilled will be considered a failure. If the
* function threw an error, it will be considered a failure.
*/
toHaveReturnedTimes(times: number): void;
/**
* Ensures that a mock function is called.
*/

View File

@@ -3977,8 +3977,110 @@ pub const Expect = struct {
return .zero;
}
pub const toHaveReturned = notImplementedJSCFn;
pub const toHaveReturnedTimes = notImplementedJSCFn;
const ReturnStatus = enum {
throw,
@"return",
incomplete,
pub const Map = bun.ComptimeEnumMap(ReturnStatus);
};
inline fn toHaveReturnedTimesFn(this: *Expect, globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame, comptime known_index: ?i32) JSC.JSValue {
JSC.markBinding(@src());
const thisValue = callframe.this();
const arguments = callframe.arguments(1).slice();
defer this.postMatch(globalObject);
const name = comptime if (known_index != null and known_index.? == 0) "toHaveReturned" else "toHaveReturnedTimes";
const value: JSValue = this.getValue(globalObject, thisValue, name, if (known_index != null and known_index.? == 0) "" else "<green>expected<r>") orelse return .zero;
incrementExpectCallCounter();
const returns = JSMockFunction__getReturns(value);
if (returns == .zero or !returns.jsType().isArray()) {
globalObject.throw("Expected value must be a mock function: {}", .{value});
return .zero;
}
const return_count: i32 = if (known_index) |index| index else brk: {
if (arguments.len < 1 or !arguments[0].isUInt32AsAnyInt()) {
globalObject.throwInvalidArguments(name ++ "() requires 1 non-negative integer argument", .{});
return .zero;
}
break :brk arguments[0].coerce(i32, globalObject);
};
var pass = false;
const index: u32 = @as(u32, @intCast(return_count)) -| 1;
const times_value = returns.getDirectIndex(
globalObject,
index,
);
const total_count = returns.getLength(globalObject);
const return_status: ReturnStatus = brk: {
// Returns is an array of:
//
// { type: "throw" | "incomplete" | "return", value: any}
//
if (total_count >= return_count and times_value.isCell()) {
if (times_value.get(globalObject, "type")) |type_string| {
if (type_string.isString()) {
break :brk ReturnStatus.Map.fromJS(globalObject, type_string) orelse {
if (!globalObject.hasException())
globalObject.throw("Expected value must be a mock function with returns: {}", .{value});
return .zero;
};
}
}
}
break :brk ReturnStatus.incomplete;
};
if (globalObject.hasException())
return .zero;
pass = return_status == ReturnStatus.@"return";
const not = this.flags.not;
if (not) pass = !pass;
if (pass) return .undefined;
if (!pass and return_status == ReturnStatus.throw) {
const signature = comptime getSignature(name, "<green>expected<r>", false);
const fmt = signature ++ "\n\n" ++ "Function threw an exception\n{any}\n";
var formatter = JSC.ConsoleObject.Formatter{
.globalThis = globalObject,
.quote_strings = true,
};
globalObject.throwPretty(fmt, .{times_value.get(globalObject, "value").?.toFmt(globalObject, &formatter)});
return .zero;
}
switch (not) {
inline else => |is_not| {
const signature = comptime getSignature(name, "<green>expected<r>", is_not);
const fmt = signature ++ "\n\n" ++ "Expected number of successful calls: <green>{d}<r>\n" ++ "Received number of calls: <red>{d}<r>\n";
globalObject.throwPretty(fmt, .{ index, total_count });
return .zero;
},
}
}
pub fn toHaveReturned(this: *Expect, globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSC.JSValue {
return toHaveReturnedTimesFn(this, globalObject, callframe, 0);
}
pub fn toHaveReturnedTimes(this: *Expect, globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSC.JSValue {
return toHaveReturnedTimesFn(this, globalObject, callframe, null);
}
pub const toHaveReturnedWith = notImplementedJSCFn;
pub const toHaveLastReturnedWith = notImplementedJSCFn;
pub const toHaveNthReturnedWith = notImplementedJSCFn;

View File

@@ -90,6 +90,32 @@ describe("mock()", () => {
expect(func).toHaveBeenCalled();
});
test("toHaveReturned()", () => {
const func = jest.fn(() => "the jedi");
expect(func).not.toHaveReturned();
func();
expect(func).toHaveReturned();
expect(func).toHaveReturnedTimes(1);
expect(func.mock.calls).toHaveLength(1);
expect(func.mock.calls[0]).toBeEmpty();
func();
expect(func).toHaveReturnedTimes(2);
const func2 = jest.fn(() => {
throw new Error("the jedi");
});
expect(func2).not.toHaveReturned();
try {
func2();
} catch (e) {}
expect(func2).not.toHaveReturned();
try {
expect(func2).toHaveReturned();
} catch (e) {
expect(e.message).toContain("Function threw an exception");
}
});
test("passes this value", () => {
const fn = jest.fn(function hey() {
"use strict";