diff options
author | 2023-05-26 19:24:20 -0700 | |
---|---|---|
committer | 2023-05-26 19:24:20 -0700 | |
commit | 1a30b4fe2903186658ef0c70e9c6cdbb5c5bed6b (patch) | |
tree | bb08e4774e1526e1fbf1fa6c920f9db5b64b31b4 | |
parent | 4298f36fc9745a130a3296a05e9577e0c9bae6fe (diff) | |
download | bun-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.ts | 10 | ||||
-rw-r--r-- | src/bun.js/bindings/ZigGeneratedClasses.cpp | 31 | ||||
-rw-r--r-- | src/bun.js/bindings/bindings.zig | 41 | ||||
-rw-r--r-- | src/bun.js/bindings/generated_classes.zig | 3 | ||||
-rw-r--r-- | src/bun.js/test/jest.classes.ts | 21 | ||||
-rw-r--r-- | src/bun.js/test/jest.zig | 89 | ||||
-rw-r--r-- | test/js/bun/test/expect.test.ts | 75 |
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); + } +} |