Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(expect): add toBeInstanceOf matcher #2389

Merged
merged 2 commits into from
Mar 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions packages/bun-types/bun-test.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,15 @@ declare module "bun:test" {
* expect(undefined).toBeDefined(); // fail
*/
toBeDefined(): void;
/**
* Asserts that the expected value is an instance of value
*
* @example
* expect([]).toBeInstanceOf(Array);
* expect(null).toBeInstanceOf(Array); // fail
*/
toBeInstanceOf(value: Function): void;

/**
* Asserts that a value is `undefined`.
*
Expand Down
65 changes: 64 additions & 1 deletion src/bun.js/test/jest.zig
Original file line number Diff line number Diff line change
Expand Up @@ -2080,6 +2080,70 @@ pub const Expect = struct {
return .zero;
}

pub fn toBeInstanceOf(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSValue {
defer this.postMatch(globalObject);

const thisValue = callFrame.this();
const _arguments = callFrame.arguments(1);
const arguments: []const JSValue = _arguments.ptr[0.._arguments.len];

if (arguments.len < 1) {
globalObject.throwInvalidArguments("toBeInstanceOf() requires 1 argument", .{});
return .zero;
}

if (this.scope.tests.items.len <= this.test_id) {
globalObject.throw("toBeInstanceOf() must be called in a test", .{});
return .zero;
}

active_test_expectation_counter.actual += 1;

const expected_value = arguments[0];
if (!expected_value.jsType().isFunction()) {
Copy link
Collaborator

@Jarred-Sumner Jarred-Sumner Mar 14, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you either add a TODO here to check that the function is a constructor or add a binding for isConstructor?

Checking that the JSType is one of the function types isn't precise enough. Functions can callable and/or constructable. The JSType can be whatever we want, though in most cases it will be one of the 3 that JSType currently checks (just not every case, in theory)

Suggested change
if (!expected_value.jsType().isFunction()) {
if (!expected_value.isConstructable()) {

This isConstructor function doesn't exist, but it would be nearly identical to isCallable:

pub fn isCallable(this: JSValue, vm: *VM) bool {
return cppFn("isCallable", .{ this, vm });
}

bool JSC__JSValue__isCallable(JSC__JSValue JSValue0, JSC__VM* arg1)
{
return JSC::JSValue::decode(JSValue0).isCallable();
}

var fmt = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true };
globalObject.throw("Expected value must be a function: {any}", .{expected_value.toFmt(globalObject, &fmt)});
return .zero;
}
expected_value.ensureStillAlive();

const value = Expect.capturedValueGetCached(thisValue) orelse {
globalObject.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{});
return .zero;
};
value.ensureStillAlive();

const not = this.op.contains(.not);
var pass = value.isInstanceOf(globalObject, expected_value);
if (not) pass = !pass;
if (pass) return thisValue;

// handle failure
var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true };
const value_fmt = value.toFmt(globalObject, &formatter);
if (not) {
const received_line = "Received: <red>{any}<r>\n";
const fmt = comptime getSignature("toBeInstanceOf", "", true) ++ "\n\n" ++ received_line;
if (Output.enable_ansi_colors) {
globalObject.throw(Output.prettyFmt(fmt, true), .{value_fmt});
return .zero;
}

globalObject.throw(Output.prettyFmt(fmt, false), .{value_fmt});
return .zero;
}

const received_line = "Received: <red>{any}<r>\n";
const fmt = comptime getSignature("toBeInstanceOf", "", false) ++ "\n\n" ++ received_line;
if (Output.enable_ansi_colors) {
globalObject.throw(Output.prettyFmt(fmt, true), .{value_fmt});
return .zero;
}

globalObject.throw(Output.prettyFmt(fmt, false), .{value_fmt});
return .zero;
}

pub const toHaveBeenCalledTimes = notImplementedJSCFn;
pub const toHaveBeenCalledWith = notImplementedJSCFn;
pub const toHaveBeenLastCalledWith = notImplementedJSCFn;
Expand All @@ -2089,7 +2153,6 @@ pub const Expect = struct {
pub const toHaveLastReturnedWith = notImplementedJSCFn;
pub const toHaveNthReturnedWith = notImplementedJSCFn;
pub const toBeCloseTo = notImplementedJSCFn;
pub const toBeInstanceOf = notImplementedJSCFn;
pub const toContainEqual = notImplementedJSCFn;
pub const toMatch = notImplementedJSCFn;
pub const toMatchObject = notImplementedJSCFn;
Expand Down
11 changes: 11 additions & 0 deletions test/bun.js/bun-test/matchers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { describe, expect, test } from "bun:test";

describe("different kinds of matchers", () => {
test("toBeInstanceOf", () => {
expect({}).toBeInstanceOf(Object);
expect(new String("test")).toBeInstanceOf(String);
expect(() => {}).toBeInstanceOf(Function);
class A {}
expect(new A()).toBeInstanceOf(A);
});
});