diff options
author | 2023-07-29 00:46:44 +0200 | |
---|---|---|
committer | 2023-07-28 15:46:44 -0700 | |
commit | 242d8655d854c1b818c38d9b021a31d673638e1e (patch) | |
tree | 701763879bc87d96aca21aedc78294cbaf89ed0e | |
parent | 9b91e3c1a25548217d846932c14e3ccdd0942a99 (diff) | |
download | bun-242d8655d854c1b818c38d9b021a31d673638e1e.tar.gz bun-242d8655d854c1b818c38d9b021a31d673638e1e.tar.zst bun-242d8655d854c1b818c38d9b021a31d673638e1e.zip |
feat(bun/test): Impl. expect().pass() & expect().fail() (#3843)
* Impl. pass & fail
* fix
* fix 2
* smol
-rw-r--r-- | packages/bun-types/bun-test.d.ts | 24 | ||||
-rw-r--r-- | src/bun.js/bindings/ZigGeneratedClasses.cpp | 62 | ||||
-rw-r--r-- | src/bun.js/bindings/generated_classes.zig | 6 | ||||
-rw-r--r-- | src/bun.js/test/expect.zig | 102 | ||||
-rw-r--r-- | src/bun.js/test/jest.classes.ts | 8 | ||||
-rw-r--r-- | test/js/bun/test/jest-extended.test.js | 15 |
6 files changed, 212 insertions, 5 deletions
diff --git a/packages/bun-types/bun-test.d.ts b/packages/bun-types/bun-test.d.ts index 5182f7e93..6877744f2 100644 --- a/packages/bun-types/bun-test.d.ts +++ b/packages/bun-types/bun-test.d.ts @@ -469,6 +469,30 @@ declare module "bun:test" { */ rejects: Expect<unknown>; /** + * Assertion which passes. + * + * @link https://jest-extended.jestcommunity.dev/docs/matchers/pass + * @example + * expect().pass(); + * expect().pass("message is optional"); + * expect().not.pass(); + * expect().not.pass("hi"); + * + * @param message the message to display if the test fails (optional) + */ + pass: (message?: string) => void; + /** + * Assertion which fails. + * + * @link https://jest-extended.jestcommunity.dev/docs/matchers/fail + * @example + * expect().fail(); + * expect().fail("message is optional"); + * expect().not.fail(); + * expect().not.fail("hi"); + */ + fail: (message?: string) => void; + /** * Asserts that a value equals what is expected. * * - For non-primitive values, like objects and arrays, diff --git a/src/bun.js/bindings/ZigGeneratedClasses.cpp b/src/bun.js/bindings/ZigGeneratedClasses.cpp index 245010158..25aca16c4 100644 --- a/src/bun.js/bindings/ZigGeneratedClasses.cpp +++ b/src/bun.js/bindings/ZigGeneratedClasses.cpp @@ -2693,9 +2693,15 @@ JSC_DECLARE_CUSTOM_GETTER(jsExpectConstructor); extern "C" void ExpectClass__finalize(void*); extern "C" JSC_DECLARE_HOST_FUNCTION(ExpectClass__call); +extern "C" EncodedJSValue ExpectPrototype__fail(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); +JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__failCallback); + extern "C" JSC::EncodedJSValue ExpectPrototype__getNot(void* ptr, JSC::EncodedJSValue thisValue, JSC::JSGlobalObject* lexicalGlobalObject); JSC_DECLARE_CUSTOM_GETTER(ExpectPrototype__notGetterWrap); +extern "C" EncodedJSValue ExpectPrototype___pass(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); +JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__passCallback); + extern "C" JSC::EncodedJSValue ExpectPrototype__getRejects(void* ptr, JSC::EncodedJSValue thisValue, JSC::JSGlobalObject* lexicalGlobalObject); JSC_DECLARE_CUSTOM_GETTER(ExpectPrototype__rejectsGetterWrap); @@ -2879,7 +2885,9 @@ JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toThrowErrorMatchingSnapshotCallback) STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSExpectPrototype, JSExpectPrototype::Base); static const HashTableValue JSExpectPrototypeTableValues[] = { + { "fail"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__failCallback, 1 } }, { "not"_s, static_cast<unsigned>(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::GetterSetterType, ExpectPrototype__notGetterWrap, 0 } }, + { "pass"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__passCallback, 1 } }, { "rejects"_s, static_cast<unsigned>(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::GetterSetterType, ExpectPrototype__rejectsGetterWrap, 0 } }, { "resolves"_s, static_cast<unsigned>(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::GetterSetterType, ExpectPrototype__resolvesGetterWrap, 0 } }, { "toBe"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toBeCallback, 1 } }, @@ -2956,6 +2964,33 @@ JSC_DEFINE_CUSTOM_GETTER(jsExpectConstructor, (JSGlobalObject * lexicalGlobalObj return JSValue::encode(globalObject->JSExpectConstructor()); } +JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__failCallback, (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__fail(thisObject->wrapped(), lexicalGlobalObject, callFrame); +} + JSC_DEFINE_CUSTOM_GETTER(ExpectPrototype__notGetterWrap, (JSGlobalObject * lexicalGlobalObject, EncodedJSValue thisValue, PropertyName attributeName)) { auto& vm = lexicalGlobalObject->vm(); @@ -2968,6 +3003,33 @@ JSC_DEFINE_CUSTOM_GETTER(ExpectPrototype__notGetterWrap, (JSGlobalObject * lexic RELEASE_AND_RETURN(throwScope, result); } +JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__passCallback, (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___pass(thisObject->wrapped(), lexicalGlobalObject, callFrame); +} + JSC_DEFINE_CUSTOM_GETTER(ExpectPrototype__rejectsGetterWrap, (JSGlobalObject * lexicalGlobalObject, EncodedJSValue thisValue, PropertyName attributeName)) { auto& vm = lexicalGlobalObject->vm(); diff --git a/src/bun.js/bindings/generated_classes.zig b/src/bun.js/bindings/generated_classes.zig index 947be7a51..e8cc701a3 100644 --- a/src/bun.js/bindings/generated_classes.zig +++ b/src/bun.js/bindings/generated_classes.zig @@ -883,8 +883,12 @@ pub const JSExpect = struct { @compileLog("Expect.finalize is not a finalizer"); } + if (@TypeOf(Expect.fail) != CallbackType) + @compileLog("Expected Expect.fail to be a callback but received " ++ @typeName(@TypeOf(Expect.fail))); if (@TypeOf(Expect.getNot) != GetterTypeWithThisValue) @compileLog("Expected Expect.getNot to be a getter with thisValue"); + if (@TypeOf(Expect._pass) != CallbackType) + @compileLog("Expected Expect._pass to be a callback but received " ++ @typeName(@TypeOf(Expect._pass))); if (@TypeOf(Expect.getRejects) != GetterTypeWithThisValue) @compileLog("Expected Expect.getRejects to be a getter with thisValue"); if (@TypeOf(Expect.getResolves) != GetterTypeWithThisValue) @@ -1037,6 +1041,7 @@ pub const JSExpect = struct { if (@TypeOf(Expect.call) != StaticCallbackType) @compileLog("Expected Expect.call to be a static callback"); if (!JSC.is_bindgen) { + @export(Expect._pass, .{ .name = "ExpectPrototype___pass" }); @export(Expect.addSnapshotSerializer, .{ .name = "ExpectClass__addSnapshotSerializer" }); @export(Expect.any, .{ .name = "ExpectClass__any" }); @export(Expect.anything, .{ .name = "ExpectClass__anything" }); @@ -1045,6 +1050,7 @@ pub const JSExpect = struct { @export(Expect.call, .{ .name = "ExpectClass__call" }); @export(Expect.constructor, .{ .name = "ExpectClass__construct" }); @export(Expect.extend, .{ .name = "ExpectClass__extend" }); + @export(Expect.fail, .{ .name = "ExpectPrototype__fail" }); @export(Expect.finalize, .{ .name = "ExpectClass__finalize" }); @export(Expect.getNot, .{ .name = "ExpectPrototype__getNot" }); @export(Expect.getRejects, .{ .name = "ExpectPrototype__getRejects" }); diff --git a/src/bun.js/test/expect.zig b/src/bun.js/test/expect.zig index d6f8ebb12..07fbca03c 100644 --- a/src/bun.js/test/expect.zig +++ b/src/bun.js/test/expect.zig @@ -289,6 +289,108 @@ pub const Expect = struct { return null; } + // pass here has a leading underscore to avoid name collision with the pass variable in other functions + pub fn _pass( + this: *Expect, + globalObject: *JSC.JSGlobalObject, + callFrame: *JSC.CallFrame, + ) callconv(.C) JSC.JSValue { + defer this.postMatch(globalObject); + + const thisValue = callFrame.this(); + const arguments_ = callFrame.arguments(1); + const arguments = arguments_.ptr[0..arguments_.len]; + + var _msg: ZigString = ZigString.Empty; + + if (arguments.len > 0) { + const value = arguments[0]; + value.ensureStillAlive(); + + if (!value.isString()) { + globalObject.throwInvalidArgumentType("pass", "message", "string"); + return .zero; + } + + value.toZigString(&_msg, globalObject); + } else { + _msg = ZigString.fromBytes("passes by .pass() assertion"); + } + + active_test_expectation_counter.actual += 1; + + const not = this.flags.not; + var pass = true; + + if (not) pass = !pass; + if (pass) return thisValue; + + var msg = _msg.toSlice(default_allocator); + defer msg.deinit(); + + if (not) { + const signature = comptime getSignature("pass", "", true); + const fmt = signature ++ "\n\n{s}\n"; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{msg.slice()}); + return .zero; + } + globalObject.throw(Output.prettyFmt(fmt, false), .{msg.slice()}); + return .zero; + } + + // should never reach here + return .zero; + } + + pub fn fail( + this: *Expect, + globalObject: *JSC.JSGlobalObject, + callFrame: *JSC.CallFrame, + ) callconv(.C) JSC.JSValue { + defer this.postMatch(globalObject); + + const thisValue = callFrame.this(); + const arguments_ = callFrame.arguments(1); + const arguments = arguments_.ptr[0..arguments_.len]; + + var _msg: ZigString = ZigString.Empty; + + if (arguments.len > 0) { + const value = arguments[0]; + value.ensureStillAlive(); + + if (!value.isString()) { + globalObject.throwInvalidArgumentType("fail", "message", "string"); + return .zero; + } + + value.toZigString(&_msg, globalObject); + } else { + _msg = ZigString.fromBytes("fails by .fail() assertion"); + } + + active_test_expectation_counter.actual += 1; + + const not = this.flags.not; + var pass = false; + + if (not) pass = !pass; + if (pass) return thisValue; + + var msg = _msg.toSlice(default_allocator); + defer msg.deinit(); + + const signature = comptime getSignature("fail", "", true); + const fmt = signature ++ "\n\n{s}\n"; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{msg.slice()}); + return .zero; + } + globalObject.throw(Output.prettyFmt(fmt, false), .{msg.slice()}); + return .zero; + } + /// Object.is() pub fn toBe( this: *Expect, diff --git a/src/bun.js/test/jest.classes.ts b/src/bun.js/test/jest.classes.ts index e5bf567a9..c337ab4ec 100644 --- a/src/bun.js/test/jest.classes.ts +++ b/src/bun.js/test/jest.classes.ts @@ -108,6 +108,14 @@ export default [ }, }, proto: { + pass: { + fn: "_pass", + length: 1, + }, + fail: { + fn: "fail", + length: 1, + }, toBe: { fn: "toBe", length: 1, diff --git a/test/js/bun/test/jest-extended.test.js b/test/js/bun/test/jest-extended.test.js index b03e0828f..5f84914f1 100644 --- a/test/js/bun/test/jest-extended.test.js +++ b/test/js/bun/test/jest-extended.test.js @@ -15,17 +15,22 @@ const inspect = isBun ? Bun.inspect : require("util").inspect; // https://jest-extended.jestcommunity.dev/docs/matchers/ describe("jest-extended", () => { - test.todo("pass()", () => { - expect(typeof expect().pass).toBe("function"); - expect(() => expect().not.pass()).toThrow(); + test("pass()", () => { + expect(expect().pass).toBeTypeOf("function"); + expect(() => expect("ignored value").not.pass()).toThrow("passes by .pass() assertion"); + expect(() => expect().not.pass("message here")).toThrow("message here"); + expect(() => expect().pass(1)).toThrow("Expected message to be a string for 'pass'."); expect().pass(); expect().pass("message ignored"); }); - test.todo("fail()", () => { - expect(typeof expect().fail).toBe("function"); + test("fail()", () => { + expect(expect().fail).toBeTypeOf("function"); expect(() => expect("ignored value").fail("message here")).toThrow("message here"); + expect(() => expect().fail()).toThrow("fails by .fail() assertion"); + expect(() => expect().fail(1)).toThrow("Expected message to be a string for 'fail'."); expect().not.fail(); + expect().not.fail("message ignored"); }); describe("toBeEmpty()", () => { |