aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Ashcon Partovi <ashcon@partovi.net> 2023-05-26 19:24:20 -0700
committerGravatar GitHub <noreply@github.com> 2023-05-26 19:24:20 -0700
commit1a30b4fe2903186658ef0c70e9c6cdbb5c5bed6b (patch)
treebb08e4774e1526e1fbf1fa6c920f9db5b64b31b4
parent4298f36fc9745a130a3296a05e9577e0c9bae6fe (diff)
downloadbun-1a30b4fe2903186658ef0c70e9c6cdbb5c5bed6b.tar.gz
bun-1a30b4fe2903186658ef0c70e9c6cdbb5c5bed6b.tar.zst
bun-1a30b4fe2903186658ef0c70e9c6cdbb5c5bed6b.zip
Implement `expect().toBeEmpty()` (#3060)
* Implement `expect().toBeEmpty()` * Fix formatting on test * Finish up expect().toBeEmpty() * Update expect.test.ts --------- Co-authored-by: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com>
-rw-r--r--packages/bun-types/bun-test.d.ts10
-rw-r--r--src/bun.js/bindings/ZigGeneratedClasses.cpp31
-rw-r--r--src/bun.js/bindings/bindings.zig41
-rw-r--r--src/bun.js/bindings/generated_classes.zig3
-rw-r--r--src/bun.js/test/jest.classes.ts21
-rw-r--r--src/bun.js/test/jest.zig89
-rw-r--r--test/js/bun/test/expect.test.ts75
7 files changed, 261 insertions, 9 deletions
diff --git a/packages/bun-types/bun-test.d.ts b/packages/bun-types/bun-test.d.ts
index b66f42fe8..425832c7f 100644
--- a/packages/bun-types/bun-test.d.ts
+++ b/packages/bun-types/bun-test.d.ts
@@ -523,6 +523,16 @@ declare module "bun:test" {
* @param hint Hint used to identify the snapshot in the snapshot file.
*/
toMatchSnapshot(propertyMatchers?: Object, hint?: string): void;
+ /**
+ * Asserts that a value is empty.
+ *
+ * @example
+ * expect("").toBeEmpty();
+ * expect([]).toBeEmpty();
+ * expect({}).toBeEmpty();
+ * expect(new Set()).toBeEmpty();
+ */
+ toBeEmpty(): void;
};
}
diff --git a/src/bun.js/bindings/ZigGeneratedClasses.cpp b/src/bun.js/bindings/ZigGeneratedClasses.cpp
index d338bcb9c..5482c461f 100644
--- a/src/bun.js/bindings/ZigGeneratedClasses.cpp
+++ b/src/bun.js/bindings/ZigGeneratedClasses.cpp
@@ -2671,6 +2671,9 @@ JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toBeCloseToCallback);
extern "C" EncodedJSValue ExpectPrototype__toBeDefined(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame);
JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toBeDefinedCallback);
+extern "C" EncodedJSValue ExpectPrototype__toBeEmpty(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame);
+JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toBeEmptyCallback);
+
extern "C" EncodedJSValue ExpectPrototype__toBeEven(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame);
JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toBeEvenCallback);
@@ -2779,6 +2782,7 @@ static const HashTableValue JSExpectPrototypeTableValues[] = {
{ "toBe"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toBeCallback, 1 } },
{ "toBeCloseTo"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toBeCloseToCallback, 1 } },
{ "toBeDefined"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toBeDefinedCallback, 0 } },
+ { "toBeEmpty"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toBeEmptyCallback, 0 } },
{ "toBeEven"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toBeEvenCallback, 0 } },
{ "toBeFalsy"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toBeFalsyCallback, 0 } },
{ "toBeGreaterThan"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toBeGreaterThanCallback, 1 } },
@@ -2945,6 +2949,33 @@ JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toBeDefinedCallback, (JSGlobalObject *
return ExpectPrototype__toBeDefined(thisObject->wrapped(), lexicalGlobalObject, callFrame);
}
+JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toBeEmptyCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame))
+{
+ auto& vm = lexicalGlobalObject->vm();
+
+ JSExpect* thisObject = jsDynamicCast<JSExpect*>(callFrame->thisValue());
+
+ if (UNLIKELY(!thisObject)) {
+ auto throwScope = DECLARE_THROW_SCOPE(vm);
+ return throwVMTypeError(lexicalGlobalObject, throwScope);
+ }
+
+ JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject);
+
+#ifdef BUN_DEBUG
+ /** View the file name of the JS file that called this function
+ * from a debugger */
+ SourceOrigin sourceOrigin = callFrame->callerSourceOrigin(vm);
+ const char* fileName = sourceOrigin.string().utf8().data();
+ static const char* lastFileName = nullptr;
+ if (lastFileName != fileName) {
+ lastFileName = fileName;
+ }
+#endif
+
+ return ExpectPrototype__toBeEmpty(thisObject->wrapped(), lexicalGlobalObject, callFrame);
+}
+
JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toBeEvenCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame))
{
auto& vm = lexicalGlobalObject->vm();
diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig
index 3bf2b9172..99c2306f4 100644
--- a/src/bun.js/bindings/bindings.zig
+++ b/src/bun.js/bindings/bindings.zig
@@ -3113,6 +3113,10 @@ pub const JSValue = enum(JSValueReprInt) {
pub const LastMaybeFalsyCellPrimitive = JSType.HeapBigInt;
pub const LastJSCObject = JSType.DerivedStringObject; // This is the last "JSC" Object type. After this, we have embedder's (e.g., WebCore) extended object types.
+ pub inline fn isString(this: JSType) bool {
+ return this == .String;
+ }
+
pub inline fn isStringLike(this: JSType) bool {
return switch (this) {
.String, .StringObject, .DerivedStringObject => true,
@@ -3127,6 +3131,42 @@ pub const JSValue = enum(JSValueReprInt) {
};
}
+ pub inline fn isArrayLike(this: JSType) bool {
+ return switch (this) {
+ .Array,
+ .DerivedArray,
+
+ .ArrayBuffer,
+ .BigInt64Array,
+ .BigUint64Array,
+ .Float32Array,
+ .Float64Array,
+ .Int16Array,
+ .Int32Array,
+ .Int8Array,
+ .Uint16Array,
+ .Uint32Array,
+ .Uint8Array,
+ .Uint8ClampedArray,
+ => true,
+ else => false,
+ };
+ }
+
+ pub inline fn isSet(this: JSType) bool {
+ return switch (this) {
+ .JSSet, .JSWeakSet => true,
+ else => false,
+ };
+ }
+
+ pub inline fn isMap(this: JSType) bool {
+ return switch (this) {
+ .JSMap, .JSWeakMap => true,
+ else => false,
+ };
+ }
+
pub inline fn isIndexable(this: JSType) bool {
return switch (this) {
.Object,
@@ -3787,6 +3827,7 @@ pub const JSValue = enum(JSValueReprInt) {
return jsType(this).isStringLike();
}
+
pub fn isBigInt(this: JSValue) bool {
return cppFn("isBigInt", .{this});
}
diff --git a/src/bun.js/bindings/generated_classes.zig b/src/bun.js/bindings/generated_classes.zig
index 43acdc98b..a30774aa1 100644
--- a/src/bun.js/bindings/generated_classes.zig
+++ b/src/bun.js/bindings/generated_classes.zig
@@ -882,6 +882,8 @@ pub const JSExpect = struct {
@compileLog("Expected Expect.toBeCloseTo to be a callback but received " ++ @typeName(@TypeOf(Expect.toBeCloseTo)));
if (@TypeOf(Expect.toBeDefined) != CallbackType)
@compileLog("Expected Expect.toBeDefined to be a callback but received " ++ @typeName(@TypeOf(Expect.toBeDefined)));
+ if (@TypeOf(Expect.toBeEmpty) != CallbackType)
+ @compileLog("Expected Expect.toBeEmpty to be a callback but received " ++ @typeName(@TypeOf(Expect.toBeEmpty)));
if (@TypeOf(Expect.toBeEven) != CallbackType)
@compileLog("Expected Expect.toBeEven to be a callback but received " ++ @typeName(@TypeOf(Expect.toBeEven)));
if (@TypeOf(Expect.toBeFalsy) != CallbackType)
@@ -1002,6 +1004,7 @@ pub const JSExpect = struct {
@export(Expect.toBe, .{ .name = "ExpectPrototype__toBe" });
@export(Expect.toBeCloseTo, .{ .name = "ExpectPrototype__toBeCloseTo" });
@export(Expect.toBeDefined, .{ .name = "ExpectPrototype__toBeDefined" });
+ @export(Expect.toBeEmpty, .{ .name = "ExpectPrototype__toBeEmpty" });
@export(Expect.toBeEven, .{ .name = "ExpectPrototype__toBeEven" });
@export(Expect.toBeFalsy, .{ .name = "ExpectPrototype__toBeFalsy" });
@export(Expect.toBeGreaterThan, .{ .name = "ExpectPrototype__toBeGreaterThan" });
diff --git a/src/bun.js/test/jest.classes.ts b/src/bun.js/test/jest.classes.ts
index 612fc0268..bc2dbb1a1 100644
--- a/src/bun.js/test/jest.classes.ts
+++ b/src/bun.js/test/jest.classes.ts
@@ -121,10 +121,6 @@ export default [
fn: "toBeCloseTo",
length: 1,
},
- toBeEven: {
- fn: "toBeEven",
- length: 0,
- },
toBeGreaterThan: {
fn: "toBeGreaterThan",
length: 1,
@@ -141,10 +137,6 @@ export default [
fn: "toBeLessThanOrEqual",
length: 1,
},
- toBeOdd: {
- fn: "toBeOdd",
- length: 0,
- },
toBeInstanceOf: {
fn: "toBeInstanceOf",
length: 1,
@@ -229,6 +221,19 @@ export default [
getter: "getRejects",
this: true,
},
+ // jest-extended
+ toBeEmpty: {
+ fn: "toBeEmpty",
+ length: 0,
+ },
+ toBeEven: {
+ fn: "toBeEven",
+ length: 0,
+ },
+ toBeOdd: {
+ fn: "toBeOdd",
+ length: 0,
+ },
},
}),
];
diff --git a/src/bun.js/test/jest.zig b/src/bun.js/test/jest.zig
index ad9cf9b5b..58a6a3efe 100644
--- a/src/bun.js/test/jest.zig
+++ b/src/bun.js/test/jest.zig
@@ -1304,7 +1304,7 @@ pub const Expect = struct {
const actual_length = value.getLengthIfPropertyExistsInternal(globalObject);
- if (actual_length == std.math.f64_max) {
+ if (actual_length == std.math.inf(f64)) {
var fmt = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true };
globalObject.throw("Received value does not have a length property: {any}", .{value.toFmt(globalObject, &fmt)});
return .zero;
@@ -3008,6 +3008,93 @@ pub const Expect = struct {
return thisValue;
}
+ pub fn toBeEmpty(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSC.JSValue {
+ defer this.postMatch(globalObject);
+
+ const thisValue = callFrame.this();
+ const value: JSValue = 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();
+
+ if (this.scope.tests.items.len <= this.test_id) {
+ globalObject.throw("toBeEmpty() must be called in a test", .{});
+ return .zero;
+ }
+
+ active_test_expectation_counter.actual += 1;
+
+ const not = this.op.contains(.not);
+ var pass = false;
+ var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true };
+
+ const actual_length = value.getLengthIfPropertyExistsInternal(globalObject);
+
+ if (actual_length == std.math.inf(f64)) {
+ if (value.jsTypeLoose().isObject()) {
+ if (value.isIterable(globalObject)) {
+ var any_properties_in_iterator = false;
+ value.forEach(globalObject, &any_properties_in_iterator, struct {
+ pub fn anythingInIterator(
+ _: *JSC.VM,
+ _: *JSGlobalObject,
+ any_: ?*anyopaque,
+ _: JSValue,
+ ) callconv(.C) void {
+ bun.cast(*bool, any_.?).* = true;
+ }
+ }.anythingInIterator);
+ pass = !any_properties_in_iterator;
+ } else {
+ var props_iter = JSC.JSPropertyIterator(.{
+ .skip_empty_name = false,
+
+ .include_value = true,
+ }).init(globalObject, value.asObjectRef());
+ defer props_iter.deinit();
+ pass = props_iter.len == 0;
+ }
+ } else {
+ const signature = comptime getSignature("toBeEmpty", "", false);
+ const fmt = signature ++ "\n\nExpected value to be a string, object, or iterable" ++
+ "\n\nReceived: <red>{any}<r>\n";
+ globalObject.throwPretty(fmt, .{value.toFmt(globalObject, &formatter)});
+ return .zero;
+ }
+ } else if (std.math.isNan(actual_length)) {
+ globalObject.throw("Received value has non-number length property: {}", .{actual_length});
+ return .zero;
+ } else {
+ pass = actual_length == 0;
+ }
+
+ if (not and pass) {
+ const signature = comptime getSignature("toBeEmpty", "", true);
+ const fmt = signature ++ "\n\nExpected value <b>not<r> to be a string, object, or iterable" ++
+ "\n\nReceived: <red>{any}<r>\n";
+ globalObject.throwPretty(fmt, .{value.toFmt(globalObject, &formatter)});
+ return .zero;
+ }
+
+ if (not) pass = !pass;
+ if (pass) return thisValue;
+
+ if (not) {
+ const signature = comptime getSignature("toBeEmpty", "", true);
+ const fmt = signature ++ "\n\nExpected value <b>not<r> to be empty" ++
+ "\n\nReceived: <red>{any}<r>\n";
+ globalObject.throwPretty(fmt, .{value.toFmt(globalObject, &formatter)});
+ return .zero;
+ }
+
+ const signature = comptime getSignature("toBeEmpty", "", false);
+ const fmt = signature ++ "\n\nExpected value to be empty" ++
+ "\n\nReceived: <red>{any}<r>\n";
+ globalObject.throwPretty(fmt, .{value.toFmt(globalObject, &formatter)});
+ return .zero;
+ }
+
pub const PropertyMatcherIterator = struct {
received_object: JSValue,
failed: bool,
diff --git a/test/js/bun/test/expect.test.ts b/test/js/bun/test/expect.test.ts
index 2fabd6e66..96896013b 100644
--- a/test/js/bun/test/expect.test.ts
+++ b/test/js/bun/test/expect.test.ts
@@ -1,3 +1,4 @@
+import { inspect } from "bun";
import { describe, test, expect } from "bun:test";
describe("expect()", () => {
@@ -127,4 +128,78 @@ describe("expect()", () => {
}
}
});
+
+ describe("toBeEmpty()", () => {
+ const values = [
+ "",
+ [],
+ {},
+ new Set(),
+ new Map(),
+ new String(),
+ new Array(),
+ new Uint8Array(),
+ new Object(),
+ Buffer.from(""),
+ Bun.file("/tmp/empty.txt"),
+ new Headers(),
+ new URLSearchParams(),
+ new FormData(),
+ (function* () {})(),
+ ];
+ for (const value of values) {
+ test(label(value), () => {
+ if (value && typeof value === "object" && value instanceof Blob) {
+ require("fs").writeFileSync("/tmp/empty.txt", "");
+ }
+
+ expect(value).toBeEmpty();
+ });
+ }
+ });
+
+ describe("not.toBeEmpty()", () => {
+ const values = [
+ " ",
+ [""],
+ [undefined],
+ { "": "" },
+ new Set([""]),
+ new Map([["", ""]]),
+ new String(" "),
+ new Array(1),
+ new Uint8Array(1),
+ Buffer.from(" "),
+ Bun.file(import.meta.path),
+ new Headers({
+ a: "b",
+ c: "d",
+ }),
+ new URL("https://example.com?d=e&f=g").searchParams,
+ (() => {
+ var a = new FormData();
+ a.append("a", "b");
+ a.append("c", "d");
+ return a;
+ })(),
+ (function* () {
+ yield "123";
+ })(),
+ ];
+ for (const value of values) {
+ test(label(value), () => {
+ expect(value).not.toBeEmpty();
+ });
+ }
+ });
});
+
+function label(value: unknown): string {
+ switch (typeof value) {
+ case "object":
+ const string = inspect(value).replace(/\n/g, "");
+ return string || '""';
+ default:
+ return JSON.stringify(value);
+ }
+}