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: Add some jest-extended matchers to bun test #13628

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
54 changes: 54 additions & 0 deletions packages/bun-types/test.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1057,6 +1057,60 @@ declare module "bun:test" {
* @param expected the expected value
*/
toContainAnyValues(expected: unknown): void;
/**
* Asserts that an `object` contain provided key value.
*
* The value must be an object
*
* @example
* const o = { a: 'foo', b: 'bar', c: 'baz' };
` * expect(o).toContainEntry(['a', 'foo']);
* expect(o).toContainEntry(['b', 'bar']);
* expect(o).toContainEntry(['c', 'baz']);
* expect(o).not.toContainEntry(['a','random']);
* @param expected the expected value
*/
toContainEntry(expected: unknown): void;

/**
* Asserts that an `object` contain array of provided key value.
*
* The value must be an object
*
* @example
* const o = { a: 'foo', b: 'bar', c: 'baz' };
* expect(o).toContainEntries([['a', 'foo']]);
* expect(o).toContainEntries([['c', 'baz'],['a', 'foo']]);
* expect(o).not.toContainEntries([['b', 'qux'],['a', 'foo'],]);
* @param expected the expected value
*/
toContainEntries(expected: unknown): void;

/**
* Asserts that an `object` contain array of all key value.
*
* The value must be an object
*
* @example
* const o = { a: 'foo', b: 'bar', c: 'baz' };
* expect(o).toContainAllEntries([['c', 'baz'],['a', 'foo'],['b','bar']]);
* expect(o).not.toContainAllEntries([['b', 'qux'],['a', 'foo'],]);
* @param expected the expected value
*/
toContainAllEntries(expected: unknown): void;

/**
* Asserts that an `object` contain array of any one or more key value.
*
* The value must be an object
*
* @example
* const o = { a: 'foo', b: 'bar', c: 'baz' };
* expect(o).toContainAnyEntries([['c', 'baz'],['a', 'invalid'],['b','one']]);
* expect(o).not.toContainAnyEntries([['b', 'qux'],['a', 'nil'],]);
* @param expected the expected value
*/
toContainAnyEntries(expected: unknown): void;

/**
* Asserts that an `object` contains all the provided keys.
Expand Down
276 changes: 276 additions & 0 deletions src/bun.js/test/expect.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1295,6 +1295,282 @@ pub const Expect = struct {
return this.throw(globalObject, comptime getSignature("toContainAnyValues", "<green>expected<r>", false), fmt, .{ expected_fmt, value_fmt });
}

pub fn toContainEntry(
this: *Expect,
globalObject: *JSGlobalObject,
callFrame: *CallFrame,
) JSValue {
defer this.postMatch(globalObject);
const thisValue = callFrame.this();
const arguments_ = callFrame.arguments(1);
const arguments = arguments_.slice();

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

incrementExpectCallCounter();

const expected = arguments[0];
if (!expected.jsType().isArray()) {
globalObject.throwInvalidArgumentType("toContainEntry", "expected", "array");
return .zero;
}
expected.ensureStillAlive();
const value: JSValue = this.getValue(globalObject, thisValue, "toContainEntry", "<green>expected<r>") orelse return .zero;

const not = this.flags.not;
var pass = false;

if (!value.isUndefinedOrNull()) {
const key = expected.getIndex(globalObject, 0);
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
const key = expected.getIndex(globalObject, 0);
const key = expected.getIndex(globalObject, 0);
if (globalObject.hasException()) return .zero;

Each getIndex call will need to check for exceptions. It's possible for the array to throw

var obj = { a: 1 };
var arr = ["a"];
Object.defineProperty(arr, 1, {
    get: function() {
        throw new Error("error");
    }
    enumerable: true,
}
expect(obj).toContainEntry(arr);

const key_string = key.toBunString(globalObject);
defer key_string.deref();

const property_value = expected.getIndex(globalObject, 1);
const accessed_property_value = value.getOwn(globalObject, key_string) orelse JSValue.undefined;
nithinkjoy-tech marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Member

Choose a reason for hiding this comment

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

I think the orelse case should set pass = false. Currently this test will pass when it should fail:

expect({}).toContainEntry([ a, 1 ]);

Copy link
Member

Choose a reason for hiding this comment

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

Applies similarly to toContainEntries, toContainAllEntries, and toContainAnyEntries

if (globalObject.hasException()) return .zero;
if (property_value.jestDeepEquals(accessed_property_value, globalObject)) pass = true;
}

if (not) pass = !pass;
if (pass) return thisValue;

// handle failure
var formatter = JSC.ConsoleObject.Formatter{ .globalThis = globalObject, .quote_strings = true };
const value_fmt = value.toFmt(&formatter);
const expected_fmt = expected.toFmt(&formatter);
if (not) {
const received_fmt = value.toFmt(&formatter);
const expected_line = "Expected object to not contain entry: <green>{any}<r>\nReceived: <red>{any}<r>\n";
const fmt = "\n\n" ++ expected_line;
this.throw(globalObject, comptime getSignature("toContainEntry", "<green>expected<r>", true), fmt, .{ expected_fmt, received_fmt });
return .zero;
}

const expected_line = "Expected object to contain entry:: <green>{any}<r>\n";
const received_line = "Received: <red>{any}<r>\n";
const fmt = "\n\n" ++ expected_line ++ received_line;
this.throw(globalObject, comptime getSignature("toContainEntry", "<green>expected<r>", false), fmt, .{ expected_fmt, value_fmt });
return .zero;
}

pub fn toContainEntries(
this: *Expect,
globalObject: *JSGlobalObject,
callFrame: *CallFrame,
) JSValue {
defer this.postMatch(globalObject);
const thisValue = callFrame.this();
const arguments_ = callFrame.arguments(1);
const arguments = arguments_.slice();

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

incrementExpectCallCounter();

const expected = arguments[0];
if (!expected.jsType().isArray()) {
globalObject.throwInvalidArgumentType("toContainEntries", "expected", "array");
return .zero;
}
expected.ensureStillAlive();
const value: JSValue = this.getValue(globalObject, thisValue, "toContainEntries", "<green>expected<r>") orelse return .zero;

const not = this.flags.not;
var pass = true;

if (!value.isUndefinedOrNull()) {
var itr = expected.arrayIterator(globalObject);

while (itr.next()) |item| {
if (!item.isUndefinedOrNull()) {
const key = item.getIndex(globalObject, 0);
const key_string = key.toBunString(globalObject);
defer key_string.deref();

const property_value = item.getIndex(globalObject, 1);
if (globalObject.hasException()) return .zero;
const accessed_property_value = value.getOwn(globalObject, key_string) orelse JSValue.undefined;
if (globalObject.hasException()) return .zero;

if (!property_value.jestDeepEquals(accessed_property_value, globalObject)) {
pass = false;
break;
}
}
}
}

if (not) pass = !pass;
if (pass) return thisValue;

// handle failure
var formatter = JSC.ConsoleObject.Formatter{ .globalThis = globalObject, .quote_strings = true };
const value_fmt = value.toFmt(&formatter);
const expected_fmt = expected.toFmt(&formatter);
if (not) {
const received_fmt = value.toFmt(&formatter);
const expected_line = "Expected object to not contain all of the given entries: <green>{any}<r>\nReceived: <red>{any}<r>\n";
const fmt = "\n\n" ++ expected_line;
this.throw(globalObject, comptime getSignature("toContainEntries", "<green>expected<r>", true), fmt, .{ expected_fmt, received_fmt });
return .zero;
}

const expected_line = "Expected object to contain all of the given entries: <green>{any}<r>\n";
const received_line = "Received: <red>{any}<r>\n";
const fmt = "\n\n" ++ expected_line ++ received_line;
this.throw(globalObject, comptime getSignature("toContainEntries", "<green>expected<r>", false), fmt, .{ expected_fmt, value_fmt });
return .zero;
}

pub fn toContainAllEntries(
this: *Expect,
globalObject: *JSGlobalObject,
callFrame: *CallFrame,
) JSValue {
defer this.postMatch(globalObject);
const thisValue = callFrame.this();
const arguments_ = callFrame.arguments(1);
const arguments = arguments_.slice();

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

incrementExpectCallCounter();

const expected = arguments[0];
if (!expected.jsType().isArray()) {
globalObject.throwInvalidArgumentType("toContainAllEntries", "expected", "array");
return .zero;
}
expected.ensureStillAlive();
const value: JSValue = this.getValue(globalObject, thisValue, "toContainAllEntries", "<green>expected<r>") orelse return .zero;

const not = this.flags.not;
var pass = true;

const count = expected.getLength(globalObject);
if (!value.isUndefinedOrNull()) {
if (value.keys(globalObject).getLength(globalObject) == count) {
var itr = expected.arrayIterator(globalObject);

while (itr.next()) |item| {
if (!item.isUndefinedOrNull()) {
const property_key = item.getIndex(globalObject, 0);
const key_string = property_key.toBunString(globalObject);
defer key_string.deref();

const property_value = item.getIndex(globalObject, 1);
const accessed_property_value = value.getOwn(globalObject, key_string) orelse JSValue.undefined;
if (globalObject.hasException()) return .zero;
if (!accessed_property_value.jestDeepEquals(property_value, globalObject)) {
pass = false;
break;
}
}
}
} else pass = false;
}

if (not) pass = !pass;
if (pass) return thisValue;

// handle failure
var formatter = JSC.ConsoleObject.Formatter{ .globalThis = globalObject, .quote_strings = true };
const value_fmt = value.toFmt(&formatter);
const expected_fmt = expected.toFmt(&formatter);
if (not) {
const received_fmt = value.toFmt(&formatter);
const expected_line = "Expected object to not only contain all of the given entries: <green>{any}<r>\nReceived: <red>{any}<r>\n";
const fmt = "\n\n" ++ expected_line;
this.throw(globalObject, comptime getSignature("toContainAllEntries", "<green>expected<r>", true), fmt, .{ expected_fmt, received_fmt });
return .zero;
}

const expected_line = "Expected object to only contain all of the given entries: <green>{any}<r>\n";
const received_line = "Received: <red>{any}<r>\n";
const fmt = "\n\n" ++ expected_line ++ received_line;
this.throw(globalObject, comptime getSignature("toContainAllEntries", "<green>expected<r>", false), fmt, .{ expected_fmt, value_fmt });
return .zero;
}

pub fn toContainAnyEntries(
this: *Expect,
globalObject: *JSGlobalObject,
callFrame: *CallFrame,
) JSValue {
defer this.postMatch(globalObject);
const thisValue = callFrame.this();
const arguments_ = callFrame.arguments(1);
const arguments = arguments_.slice();

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

incrementExpectCallCounter();

const expected = arguments[0];
if (!expected.jsType().isArray()) {
globalObject.throwInvalidArgumentType("toContainAnyEntries", "expected", "array");
return .zero;
}
expected.ensureStillAlive();
const value: JSValue = this.getValue(globalObject, thisValue, "toContainAnyEntries", "<green>expected<r>") orelse return .zero;

const not = this.flags.not;
var pass = false;

if (!value.isUndefinedOrNull()) {
var itr = expected.arrayIterator(globalObject);

while (itr.next()) |item| {
if (!item.isUndefinedOrNull()) {
const key = item.getIndex(globalObject, 0);
const key_string = key.toBunString(globalObject);
defer key_string.deref();

const property_value = item.getIndex(globalObject, 1);
const accessed_property_value = value.getOwn(globalObject, key_string) orelse JSValue.undefined;
if (globalObject.hasException()) return .zero;
if (property_value.jestDeepEquals(accessed_property_value, globalObject)) {
pass = true;
break;
}
}
}
}

if (not) pass = !pass;
if (pass) return thisValue;

// handle failure
var formatter = JSC.ConsoleObject.Formatter{ .globalThis = globalObject, .quote_strings = true };
const value_fmt = value.toFmt(&formatter);
const expected_fmt = expected.toFmt(&formatter);
if (not) {
const received_fmt = value.toFmt(&formatter);
const expected_line = "Expected object to not contain any of the provided entries: <green>{any}<r>\nReceived: <red>{any}<r>\n";
const fmt = "\n\n" ++ expected_line;
this.throw(globalObject, comptime getSignature("toContainAnyEntries", "<green>expected<r>", true), fmt, .{ expected_fmt, received_fmt });
return .zero;
}

const expected_line = "Expected object to contain any of the provided entries: <green>{any}<r>\n";
const received_line = "Received: <red>{any}<r>\n";
const fmt = "\n\n" ++ expected_line ++ received_line;
this.throw(globalObject, comptime getSignature("toContainAnyEntries", "<green>expected<r>", false), fmt, .{ expected_fmt, value_fmt });
return .zero;
}

pub fn toContainEqual(
this: *Expect,
globalThis: *JSGlobalObject,
Expand Down
16 changes: 16 additions & 0 deletions src/bun.js/test/jest.classes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,22 @@ export default [
fn: "toContainAnyValues",
length: 1,
},
toContainEntry: {
fn: "toContainEntry",
length: 1,
},
toContainEntries: {
fn: "toContainEntries",
length: 1,
},
toContainAllEntries: {
fn: "toContainAllEntries",
length: 1,
},
toContainAnyEntries: {
fn: "toContainAnyEntries",
length: 1,
},
toContainKeys: {
fn: "toContainKeys",
length: 1,
Expand Down
Loading