diff options
-rw-r--r-- | packages/bun-types/bun-test.d.ts | 17 | ||||
-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 | 200 | ||||
-rw-r--r-- | src/bun.js/test/jest.classes.ts | 8 | ||||
-rw-r--r-- | test/js/bun/test/jest-extended.test.js | 76 |
6 files changed, 367 insertions, 2 deletions
diff --git a/packages/bun-types/bun-test.d.ts b/packages/bun-types/bun-test.d.ts index 680d17d2a..f6e968906 100644 --- a/packages/bun-types/bun-test.d.ts +++ b/packages/bun-types/bun-test.d.ts @@ -982,6 +982,23 @@ declare module "bun:test" { */ toInclude(expected: string): void; /** + * Asserts that a value includes a `string` {times} times. + * @param expected the expected substring + * @param times the number of times the substring should occur + */ + toIncludeRepeated(expected: string, times: number): void; + /** + * Checks whether a value satisfies a custom condition. + * @param {Function} predicate - The custom condition to be satisfied. It should be a function that takes a value as an argument (in this case the value from expect) and returns a boolean. + * @example + * expect(1).toSatisfy((val) => val > 0); + * expect("foo").toSatisfy((val) => val === "foo"); + * expect("bar").not.toSatisfy((val) => val === "bun"); + * @link https://vitest.dev/api/expect.html#tosatisfy + * @link https://jest-extended.jestcommunity.dev/docs/matchers/toSatisfy + */ + toSatisfy(predicate: (value: T) => boolean): void; + /** * Asserts that a value starts with a `string`. * * @param expected the string to start with diff --git a/src/bun.js/bindings/ZigGeneratedClasses.cpp b/src/bun.js/bindings/ZigGeneratedClasses.cpp index 25aca16c4..c2780bd7f 100644 --- a/src/bun.js/bindings/ZigGeneratedClasses.cpp +++ b/src/bun.js/bindings/ZigGeneratedClasses.cpp @@ -2855,6 +2855,9 @@ JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toHaveReturnedWithCallback); extern "C" EncodedJSValue ExpectPrototype__toInclude(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toIncludeCallback); +extern "C" EncodedJSValue ExpectPrototype__toIncludeRepeated(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); +JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toIncludeRepeatedCallback); + extern "C" EncodedJSValue ExpectPrototype__toMatch(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toMatchCallback); @@ -2867,6 +2870,9 @@ JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toMatchObjectCallback); extern "C" EncodedJSValue ExpectPrototype__toMatchSnapshot(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toMatchSnapshotCallback); +extern "C" EncodedJSValue ExpectPrototype__toSatisfy(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); +JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toSatisfyCallback); + extern "C" EncodedJSValue ExpectPrototype__toStartWith(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toStartWithCallback); @@ -2939,10 +2945,12 @@ static const HashTableValue JSExpectPrototypeTableValues[] = { { "toHaveReturnedTimes"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toHaveReturnedTimesCallback, 1 } }, { "toHaveReturnedWith"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toHaveReturnedWithCallback, 1 } }, { "toInclude"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toIncludeCallback, 1 } }, + { "toIncludeRepeated"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toIncludeRepeatedCallback, 2 } }, { "toMatch"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toMatchCallback, 1 } }, { "toMatchInlineSnapshot"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toMatchInlineSnapshotCallback, 1 } }, { "toMatchObject"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toMatchObjectCallback, 1 } }, { "toMatchSnapshot"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toMatchSnapshotCallback, 1 } }, + { "toSatisfy"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toSatisfyCallback, 1 } }, { "toStartWith"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toStartWithCallback, 1 } }, { "toStrictEqual"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toStrictEqualCallback, 1 } }, { "toThrow"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toThrowCallback, 1 } }, @@ -4377,6 +4385,33 @@ JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toIncludeCallback, (JSGlobalObject * l return ExpectPrototype__toInclude(thisObject->wrapped(), lexicalGlobalObject, callFrame); } +JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toIncludeRepeatedCallback, (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__toIncludeRepeated(thisObject->wrapped(), lexicalGlobalObject, callFrame); +} + JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toMatchCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) { auto& vm = lexicalGlobalObject->vm(); @@ -4485,6 +4520,33 @@ JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toMatchSnapshotCallback, (JSGlobalObje return ExpectPrototype__toMatchSnapshot(thisObject->wrapped(), lexicalGlobalObject, callFrame); } +JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toSatisfyCallback, (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__toSatisfy(thisObject->wrapped(), lexicalGlobalObject, callFrame); +} + JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toStartWithCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) { auto& vm = lexicalGlobalObject->vm(); diff --git a/src/bun.js/bindings/generated_classes.zig b/src/bun.js/bindings/generated_classes.zig index e8cc701a3..9cf63a2e1 100644 --- a/src/bun.js/bindings/generated_classes.zig +++ b/src/bun.js/bindings/generated_classes.zig @@ -991,6 +991,8 @@ pub const JSExpect = struct { @compileLog("Expected Expect.toHaveReturnedWith to be a callback but received " ++ @typeName(@TypeOf(Expect.toHaveReturnedWith))); if (@TypeOf(Expect.toInclude) != CallbackType) @compileLog("Expected Expect.toInclude to be a callback but received " ++ @typeName(@TypeOf(Expect.toInclude))); + if (@TypeOf(Expect.toIncludeRepeated) != CallbackType) + @compileLog("Expected Expect.toIncludeRepeated to be a callback but received " ++ @typeName(@TypeOf(Expect.toIncludeRepeated))); if (@TypeOf(Expect.toMatch) != CallbackType) @compileLog("Expected Expect.toMatch to be a callback but received " ++ @typeName(@TypeOf(Expect.toMatch))); if (@TypeOf(Expect.toMatchInlineSnapshot) != CallbackType) @@ -999,6 +1001,8 @@ pub const JSExpect = struct { @compileLog("Expected Expect.toMatchObject to be a callback but received " ++ @typeName(@TypeOf(Expect.toMatchObject))); if (@TypeOf(Expect.toMatchSnapshot) != CallbackType) @compileLog("Expected Expect.toMatchSnapshot to be a callback but received " ++ @typeName(@TypeOf(Expect.toMatchSnapshot))); + if (@TypeOf(Expect.toSatisfy) != CallbackType) + @compileLog("Expected Expect.toSatisfy to be a callback but received " ++ @typeName(@TypeOf(Expect.toSatisfy))); if (@TypeOf(Expect.toStartWith) != CallbackType) @compileLog("Expected Expect.toStartWith to be a callback but received " ++ @typeName(@TypeOf(Expect.toStartWith))); if (@TypeOf(Expect.toStrictEqual) != CallbackType) @@ -1111,10 +1115,12 @@ pub const JSExpect = struct { @export(Expect.toHaveReturnedTimes, .{ .name = "ExpectPrototype__toHaveReturnedTimes" }); @export(Expect.toHaveReturnedWith, .{ .name = "ExpectPrototype__toHaveReturnedWith" }); @export(Expect.toInclude, .{ .name = "ExpectPrototype__toInclude" }); + @export(Expect.toIncludeRepeated, .{ .name = "ExpectPrototype__toIncludeRepeated" }); @export(Expect.toMatch, .{ .name = "ExpectPrototype__toMatch" }); @export(Expect.toMatchInlineSnapshot, .{ .name = "ExpectPrototype__toMatchInlineSnapshot" }); @export(Expect.toMatchObject, .{ .name = "ExpectPrototype__toMatchObject" }); @export(Expect.toMatchSnapshot, .{ .name = "ExpectPrototype__toMatchSnapshot" }); + @export(Expect.toSatisfy, .{ .name = "ExpectPrototype__toSatisfy" }); @export(Expect.toStartWith, .{ .name = "ExpectPrototype__toStartWith" }); @export(Expect.toStrictEqual, .{ .name = "ExpectPrototype__toStrictEqual" }); @export(Expect.toThrow, .{ .name = "ExpectPrototype__toThrow" }); diff --git a/src/bun.js/test/expect.zig b/src/bun.js/test/expect.zig index c56818efc..de3f185d5 100644 --- a/src/bun.js/test/expect.zig +++ b/src/bun.js/test/expect.zig @@ -2812,6 +2812,206 @@ pub const Expect = struct { return .zero; } + pub fn toIncludeRepeated(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalThis); + + const thisValue = callFrame.this(); + const arguments_ = callFrame.arguments(2); + const arguments = arguments_.ptr[0..arguments_.len]; + + if (arguments.len < 2) { + globalThis.throwInvalidArguments("toIncludeRepeated() requires 2 arguments", .{}); + return .zero; + } + + if (this.scope.tests.items.len <= this.test_id) { + globalThis.throw("toIncludeRepeated() must be called in a test", .{}); + return .zero; + } + + active_test_expectation_counter.actual += 1; + + const substring = arguments[0]; + substring.ensureStillAlive(); + + if (!substring.isString()) { + globalThis.throw("toIncludeRepeated() requires the first argument to be a string", .{}); + return .zero; + } + + const count = arguments[1]; + count.ensureStillAlive(); + + if (!count.isAnyInt()) { + globalThis.throw("toIncludeRepeated() requires the second argument to be a number", .{}); + return .zero; + } + + const countAsNum = count.toU32(); + + const expect_string = Expect.capturedValueGetCached(thisValue) orelse { + globalThis.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); + return .zero; + }; + + if (!expect_string.isString()) { + globalThis.throw("toIncludeRepeated() requires the expect(value) to be a string", .{}); + return .zero; + } + + const not = this.flags.not; + var pass = false; + + const _expectStringAsStr = expect_string.toSliceOrNull(globalThis) orelse return .zero; + const _subStringAsStr = substring.toSliceOrNull(globalThis) orelse return .zero; + + defer { + _expectStringAsStr.deinit(); + _subStringAsStr.deinit(); + } + + var expectStringAsStr = _expectStringAsStr.slice(); + var subStringAsStr = _subStringAsStr.slice(); + + if (subStringAsStr.len == 0) { + globalThis.throw("toIncludeRepeated() requires the first argument to be a non-empty string", .{}); + return .zero; + } + + if (countAsNum == 0) + pass = !strings.contains(expectStringAsStr, subStringAsStr) + else + pass = std.mem.containsAtLeast(u8, expectStringAsStr, countAsNum, subStringAsStr); + + if (not) pass = !pass; + if (pass) return thisValue; + + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + const expect_string_fmt = expect_string.toFmt(globalThis, &formatter); + const substring_fmt = substring.toFmt(globalThis, &formatter); + const times_fmt = count.toFmt(globalThis, &formatter); + + const received_line = "Received: <red>{any}<r>\n"; + + if (not) { + if (countAsNum == 0) { + const expected_line = "Expected to include: <green>{any}<r> \n"; + const fmt = comptime getSignature("toIncludeRepeated", "<green>expected<r>", true) ++ "\n\n" ++ expected_line ++ received_line; + globalThis.throwPretty(fmt, .{ substring_fmt, expect_string_fmt }); + } else if (countAsNum == 1) { + const expected_line = "Expected not to include: <green>{any}<r> \n"; + const fmt = comptime getSignature("toIncludeRepeated", "<green>expected<r>", true) ++ "\n\n" ++ expected_line ++ received_line; + globalThis.throwPretty(fmt, .{ substring_fmt, expect_string_fmt }); + } else { + const expected_line = "Expected not to include: <green>{any}<r> <green>{any}<r> times \n"; + const fmt = comptime getSignature("toIncludeRepeated", "<green>expected<r>", true) ++ "\n\n" ++ expected_line ++ received_line; + globalThis.throwPretty(fmt, .{ substring_fmt, times_fmt, expect_string_fmt }); + } + + return .zero; + } + + if (countAsNum == 0) { + const expected_line = "Expected to not include: <green>{any}<r>\n"; + const fmt = comptime getSignature("toIncludeRepeated", "<green>expected<r>", false) ++ "\n\n" ++ expected_line ++ received_line; + globalThis.throwPretty(fmt, .{ substring_fmt, expect_string_fmt }); + } else if (countAsNum == 1) { + const expected_line = "Expected to include: <green>{any}<r>\n"; + const fmt = comptime getSignature("toIncludeRepeated", "<green>expected<r>", false) ++ "\n\n" ++ expected_line ++ received_line; + globalThis.throwPretty(fmt, .{ substring_fmt, expect_string_fmt }); + } else { + const expected_line = "Expected to include: <green>{any}<r> <green>{any}<r> times \n"; + const fmt = comptime getSignature("toIncludeRepeated", "<green>expected<r>", false) ++ "\n\n" ++ expected_line ++ received_line; + globalThis.throwPretty(fmt, .{ substring_fmt, times_fmt, expect_string_fmt }); + } + + return .zero; + } + + pub fn toSatisfy(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalThis); + + const thisValue = callFrame.this(); + const arguments_ = callFrame.arguments(1); + const arguments = arguments_.ptr[0..arguments_.len]; + + if (arguments.len < 1) { + globalThis.throwInvalidArguments("toSatisfy() requires 1 argument", .{}); + return .zero; + } + + if (this.scope.tests.items.len <= this.test_id) { + globalThis.throw("toSatisfy() must be called in a test", .{}); + return .zero; + } + + active_test_expectation_counter.actual += 1; + + const predicate = arguments[0]; + predicate.ensureStillAlive(); + + if (!predicate.isCallable(globalThis.vm())) { + globalThis.throw("toSatisfy() argument must be a function", .{}); + return .zero; + } + + const value = Expect.capturedValueGetCached(thisValue) orelse { + globalThis.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); + return .zero; + }; + value.ensureStillAlive(); + + const result = predicate.call(globalThis, &.{value}); + + if (result.toError()) |err| { + var errors: [1]*anyopaque = undefined; + var _err = errors[0..errors.len]; + + _err[0] = err.asVoid(); + + const fmt = ZigString.init("toSatisfy() predicate threw an exception"); + globalThis.vm().throwError(globalThis, globalThis.createAggregateError(_err.ptr, _err.len, &fmt)); + return .zero; + } + + const not = this.flags.not; + const pass = (result.isBoolean() and result.toBoolean()) != not; + + if (pass) return thisValue; + + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + + if (not) { + const signature = comptime getSignature("toSatisfy", "<green>expected<r>", true); + const fmt = signature ++ "\n\nExpected: not <green>{any}<r>\n"; + if (Output.enable_ansi_colors) { + globalThis.throw(Output.prettyFmt(fmt, true), .{predicate.toFmt(globalThis, &formatter)}); + return .zero; + } + globalThis.throw(Output.prettyFmt(fmt, false), .{predicate.toFmt(globalThis, &formatter)}); + return .zero; + } + + const signature = comptime getSignature("toSatisfy", "<green>expected<r>", false); + + const fmt = signature ++ "\n\nExpected: <green>{any}<r>\nReceived: <red>{any}<r>\n"; + + if (Output.enable_ansi_colors) { + globalThis.throw(Output.prettyFmt(fmt, true), .{ + predicate.toFmt(globalThis, &formatter), + value.toFmt(globalThis, &formatter), + }); + return .zero; + } + + globalThis.throw(Output.prettyFmt(fmt, false), .{ + predicate.toFmt(globalThis, &formatter), + value.toFmt(globalThis, &formatter), + }); + + return .zero; + } + pub fn toStartWith(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { defer this.postMatch(globalThis); diff --git a/src/bun.js/test/jest.classes.ts b/src/bun.js/test/jest.classes.ts index c337ab4ec..d40acbf07 100644 --- a/src/bun.js/test/jest.classes.ts +++ b/src/bun.js/test/jest.classes.ts @@ -353,6 +353,14 @@ export default [ fn: "toInclude", length: 1, }, + toIncludeRepeated: { + fn: "toIncludeRepeated", + length: 2, + }, + toSatisfy: { + fn: "toSatisfy", + length: 1, + }, toStartWith: { fn: "toStartWith", length: 1, diff --git a/test/js/bun/test/jest-extended.test.js b/test/js/bun/test/jest-extended.test.js index 2cdec75fa..816863218 100644 --- a/test/js/bun/test/jest-extended.test.js +++ b/test/js/bun/test/jest-extended.test.js @@ -110,7 +110,37 @@ describe("jest-extended", () => { expect({}).not.toBeNil(); }); - // test('toSatisfy()') + test("toSatisfy()", () => { + // Arrow functions + const isOdd = value => value % 2 === 1; + const hasLetterH = (value) => value.includes("H"); + + expect(1).toSatisfy(isOdd); + expect("Hello").toSatisfy(hasLetterH); + + // Function expressions + function hasBunInAnArray(value) { return value.includes("bun"); } + + expect(["bun", "cheese", "patty"]).toSatisfy(hasBunInAnArray); + expect(["cheese", "patty"]).not.toSatisfy(hasBunInAnArray); + + // Inline functions + expect([]).toSatisfy((value) => value.length === 0); + expect([]).not.toSatisfy(value => value.length > 0); + + // Some other types + const fooIsBar = (value) => value?.foo === "bar"; + + expect({ foo: "bar" }).toSatisfy(fooIsBar); + expect({ foo: "bun" }).not.toSatisfy(fooIsBar); + expect({ bar: "foo" }).not.toSatisfy(fooIsBar); + + // Test errors + // @ts-expect-error + expect(() => expect(1).toSatisfy(() => new Error('Bun!'))).toThrow('predicate threw an exception'); + // @ts-expect-error + expect(() => expect(1).not.toSatisfy(() => new Error('Bun!'))).toThrow('predicate threw an exception'); + }); // Array @@ -509,7 +539,49 @@ describe("jest-extended", () => { expect("bob").not.toInclude("alice"); }); - // test("toIncludeRepeated()") + test("toIncludeRepeated()", () => { + // 0 + expect("a").toIncludeRepeated("b", 0) + expect("b").not.toIncludeRepeated("b", 0); + + // 1 + expect("abc").toIncludeRepeated("a", 1); + expect("abc").not.toIncludeRepeated("d", 1); + + // Any other number + expect("abc abc abc").toIncludeRepeated("abc", 1); + expect("abc abc abc").toIncludeRepeated("abc", 2); + expect("abc abc abc").toIncludeRepeated("abc", 3); + expect("abc abc abc").not.toIncludeRepeated("abc", 4); + + // Emojis/Unicode + expect("ππ₯³π€ππ₯³").toIncludeRepeated("π", 1); + expect("ππ₯³π€ππ₯³").toIncludeRepeated("π₯³", 2); + expect("ππ₯³π€ππ₯³").not.toIncludeRepeated("π", 3); + expect("ππ₯³π€ππ₯³").not.toIncludeRepeated("πΆβπ«οΈ", 1); + + // Empty string + expect("").not.toIncludeRepeated("a", 1); + + // if toIncludeRepeated() is called with a empty string, it should throw an error or else it segfaults + expect(() => expect("a").not.toIncludeRepeated("", 1)).toThrow() + + // Just to make sure it doesn't throw an error + expect("").not.toIncludeRepeated("a", 1) + expect("").not.toIncludeRepeated("πΆβπ«οΈ", 1) + + // Expect them to throw an error + const tstErr = (y) => { return expect("").toIncludeRepeated("a", y) }; + + expect(() => tstErr(1.23)).toThrow(); + expect(() => tstErr(Infinity)).toThrow(); + expect(() => tstErr(NaN)).toThrow(); + expect(() => tstErr(-0)).toThrow(); // -0 and below (-1, -2, ...) + expect(() => tstErr(null)).toThrow(); + expect(() => tstErr(undefined)).toThrow(); + expect(() => tstErr({})).toThrow(); + }) ; + // test("toIncludeMultiple()") // test("toEqualIgnoringWhitespace()") |