From 74e135123e4fb9a3da79b8ff96c7ef802bdef6e0 Mon Sep 17 00:00:00 2001 From: Elad Bezalel Date: Wed, 4 Oct 2023 15:31:42 +0300 Subject: [PATCH 1/2] feat(test): implement `toEqualIgnoringWhitespace` --- packages/bun-types/bun-test.d.ts | 10 ++++ src/bun.js/bindings/ZigGeneratedClasses.cpp | 32 +++++++++++++ src/bun.js/bindings/generated_classes.zig | 3 ++ src/bun.js/test/expect.zig | 51 +++++++++++++++++++++ src/bun.js/test/jest.classes.ts | 4 ++ src/string_immutable.zig | 12 +++++ test/js/bun/test/expect.test.js | 12 +++++ test/js/bun/test/jest-extended.test.js | 12 ++++- 8 files changed, 135 insertions(+), 1 deletion(-) diff --git a/packages/bun-types/bun-test.d.ts b/packages/bun-types/bun-test.d.ts index a78c74b85c0cb3..47380f63cd1201 100644 --- a/packages/bun-types/bun-test.d.ts +++ b/packages/bun-types/bun-test.d.ts @@ -982,6 +982,16 @@ declare module "bun:test" { * @param end the end number (exclusive) */ toBeWithin(start: number, end: number): void; + /** + * Asserts that a value is equal to the expected string, ignoring any whitespace. + * + * @example + * expect(" foo ").toEqualIgnoringWhitespace("foo"); + * expect("bar").toEqualIgnoringWhitespace(" bar "); + * + * @param expected the expected string + */ + toEqualIgnoringWhitespace(expected: string): void; /** * Asserts that a value is a `symbol`. * diff --git a/src/bun.js/bindings/ZigGeneratedClasses.cpp b/src/bun.js/bindings/ZigGeneratedClasses.cpp index f8541ac1e05ab2..6e3a2e1e52b84d 100644 --- a/src/bun.js/bindings/ZigGeneratedClasses.cpp +++ b/src/bun.js/bindings/ZigGeneratedClasses.cpp @@ -7321,6 +7321,9 @@ JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toEndWithCallback); extern "C" EncodedJSValue ExpectPrototype__toEqual(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toEqualCallback); +extern "C" EncodedJSValue ExpectPrototype__toEqualIgnoringWhitespace(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); +JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toEqualIgnoringWhitespaceCallback); + extern "C" EncodedJSValue ExpectPrototype__toHaveBeenCalled(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); JSC_DECLARE_HOST_FUNCTION(ExpectPrototype__toHaveBeenCalledCallback); @@ -7435,6 +7438,7 @@ static const HashTableValue JSExpectPrototypeTableValues[] = { { "toContainEqual"_s, static_cast(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toContainEqualCallback, 1 } }, { "toEndWith"_s, static_cast(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toEndWithCallback, 1 } }, { "toEqual"_s, static_cast(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toEqualCallback, 1 } }, + { "toEqualIgnoringWhitespace"_s, static_cast(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toEqualIgnoringWhitespaceCallback, 1 } }, { "toHaveBeenCalled"_s, static_cast(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toHaveBeenCalledCallback, 0 } }, { "toHaveBeenCalledTimes"_s, static_cast(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toHaveBeenCalledTimesCallback, 1 } }, { "toHaveBeenCalledWith"_s, static_cast(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ExpectPrototype__toHaveBeenCalledWithCallback, 1 } }, @@ -8602,6 +8606,34 @@ JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toEqualCallback, (JSGlobalObject * lex return ExpectPrototype__toEqual(thisObject->wrapped(), lexicalGlobalObject, callFrame); } +JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toEqualIgnoringWhitespaceCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = lexicalGlobalObject->vm(); + + JSExpect* thisObject = jsDynamicCast(callFrame->thisValue()); + + if (UNLIKELY(!thisObject)) { + auto throwScope = DECLARE_THROW_SCOPE(vm); + throwVMTypeError(lexicalGlobalObject, throwScope, "Expected 'this' to be instanceof Expect"_s); + return JSValue::encode({}); + } + + 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__toEqualIgnoringWhitespace(thisObject->wrapped(), lexicalGlobalObject, callFrame); +} + JSC_DEFINE_HOST_FUNCTION(ExpectPrototype__toHaveBeenCalledCallback, (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 fc1aa2be6939f1..c44d554fc6da4d 100644 --- a/src/bun.js/bindings/generated_classes.zig +++ b/src/bun.js/bindings/generated_classes.zig @@ -2211,6 +2211,8 @@ pub const JSExpect = struct { @compileLog("Expected Expect.toEndWith to be a callback but received " ++ @typeName(@TypeOf(Expect.toEndWith))); if (@TypeOf(Expect.toEqual) != CallbackType) @compileLog("Expected Expect.toEqual to be a callback but received " ++ @typeName(@TypeOf(Expect.toEqual))); + if (@TypeOf(Expect.toEqualIgnoringWhitespace) != CallbackType) + @compileLog("Expected Expect.toEqualIgnoringWhitespace to be a callback but received " ++ @typeName(@TypeOf(Expect.toEqualIgnoringWhitespace))); if (@TypeOf(Expect.toHaveBeenCalled) != CallbackType) @compileLog("Expected Expect.toHaveBeenCalled to be a callback but received " ++ @typeName(@TypeOf(Expect.toHaveBeenCalled))); if (@TypeOf(Expect.toHaveBeenCalledTimes) != CallbackType) @@ -2347,6 +2349,7 @@ pub const JSExpect = struct { @export(Expect.toContainEqual, .{ .name = "ExpectPrototype__toContainEqual" }); @export(Expect.toEndWith, .{ .name = "ExpectPrototype__toEndWith" }); @export(Expect.toEqual, .{ .name = "ExpectPrototype__toEqual" }); + @export(Expect.toEqualIgnoringWhitespace, .{ .name = "ExpectPrototype__toEqualIgnoringWhitespace" }); @export(Expect.toHaveBeenCalled, .{ .name = "ExpectPrototype__toHaveBeenCalled" }); @export(Expect.toHaveBeenCalledTimes, .{ .name = "ExpectPrototype__toHaveBeenCalledTimes" }); @export(Expect.toHaveBeenCalledWith, .{ .name = "ExpectPrototype__toHaveBeenCalledWith" }); diff --git a/src/bun.js/test/expect.zig b/src/bun.js/test/expect.zig index 04b8670d25a24e..2955955a20e861 100644 --- a/src/bun.js/test/expect.zig +++ b/src/bun.js/test/expect.zig @@ -2649,6 +2649,57 @@ pub const Expect = struct { return .zero; } + pub fn toEqualIgnoringWhitespace(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalThis); + + const thisValue = callFrame.this(); + const _arguments = callFrame.arguments(1); + const arguments: []const JSValue = _arguments.ptr[0.._arguments.len]; + + if (arguments.len < 1) { + globalThis.throwInvalidArguments("toEqualIgnoringWhitespace() requires 1 argument", .{}); + return .zero; + } + + active_test_expectation_counter.actual += 1; + + const expected = arguments[0]; + const value: JSValue = this.getValue(globalThis, thisValue, "toEqualIgnoringWhitespace", "expected") orelse return .zero; + + const not = this.flags.not; + var pass = value.isString() and expected.isString(); + + if (pass) { + var expectedStr = expected.toString(globalThis).toSlice(globalThis, default_allocator).slice(); + var valueStr = value.toString(globalThis).toSlice(globalThis, default_allocator).slice(); + + // Remove all whitespace from both strings + expectedStr = strings.removeWhitespace(default_allocator, expectedStr); + valueStr = strings.removeWhitespace(default_allocator, valueStr); + + // Compare the strings + pass = strings.eql(expectedStr, valueStr); + } + + if (not) pass = !pass; + if (pass) return thisValue; + + // handle failure + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalThis, .quote_strings = true }; + const expected_fmt = expected.toFmt(globalThis, &formatter); + const value_fmt = value.toFmt(globalThis, &formatter); + + if (not) { + const fmt = comptime getSignature("toEqualIgnoringWhitespace", "expected", true) ++ "\n\n" ++ "Expected: not {any}\n" ++ "Received: {any}\n"; + globalThis.throwPretty(fmt, .{ expected_fmt, value_fmt }); + return .zero; + } + + const fmt = comptime getSignature("toEqualIgnoringWhitespace", "expected", false) ++ "\n\n" ++ "Expected: {any}\n" ++ "Received: {any}\n"; + globalThis.throwPretty(fmt, .{ expected_fmt, value_fmt }); + return .zero; + } + pub fn toBeSymbol(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 186c8f9a8f192e..f10d9fb3728482 100644 --- a/src/bun.js/test/jest.classes.ts +++ b/src/bun.js/test/jest.classes.ts @@ -345,6 +345,10 @@ export default [ fn: "toBeWithin", length: 2, }, + toEqualIgnoringWhitespace: { + fn: "toEqualIgnoringWhitespace", + length: 1, + }, toBeSymbol: { fn: "toBeSymbol", length: 0, diff --git a/src/string_immutable.zig b/src/string_immutable.zig index 00ee8d8355a590..02480aeb552c6d 100644 --- a/src/string_immutable.zig +++ b/src/string_immutable.zig @@ -4847,3 +4847,15 @@ pub fn concatIfNeeded( } std.debug.assert(remain.len == 0); } + +pub fn removeWhitespace(allocator: std.mem.Allocator, str: []const u8) []u8 { + var result = allocator.alloc(u8, str.len) catch unreachable; + var j: usize = 0; + for (str) |char| { + if (!std.ascii.isWhitespace(char)) { + result[j] = char; + j += 1; + } + } + return result[0..j]; +} diff --git a/test/js/bun/test/expect.test.js b/test/js/bun/test/expect.test.js index 8c9959d018bfb7..3483ed49a5f005 100644 --- a/test/js/bun/test/expect.test.js +++ b/test/js/bun/test/expect.test.js @@ -3080,6 +3080,18 @@ describe("expect()", () => { expect(Infinity).not.toBeWithin(-Infinity, Infinity); }); + test("toEqualIgnoringWhitespace()", () => { + expect("hello world").toEqualIgnoringWhitespace("hello world"); + expect(" hello world ").toEqualIgnoringWhitespace("hello world"); + expect(" h e l l o w o r l d ").toEqualIgnoringWhitespace("hello world"); + expect(" hello\nworld ").toEqualIgnoringWhitespace("hello\nworld"); + expect(`h + e + l + l + o`).toEqualIgnoringWhitespace("hello"); + }); + test("toBeSymbol()", () => { expect(Symbol()).toBeSymbol(); expect(Symbol("")).toBeSymbol(); diff --git a/test/js/bun/test/jest-extended.test.js b/test/js/bun/test/jest-extended.test.js index 7e018d310475b7..6f3d20fa960533 100644 --- a/test/js/bun/test/jest-extended.test.js +++ b/test/js/bun/test/jest-extended.test.js @@ -587,7 +587,17 @@ describe("jest-extended", () => { }); // test("toIncludeMultiple()") - // test("toEqualIgnoringWhitespace()") + test("toEqualIgnoringWhitespace()", () => { + expect("hello world").toEqualIgnoringWhitespace("hello world"); + expect(" hello world ").toEqualIgnoringWhitespace("hello world"); + expect(" h e l l o w o r l d ").toEqualIgnoringWhitespace("hello world"); + expect(" hello\nworld ").toEqualIgnoringWhitespace("hello\nworld"); + expect(`h + e + l + l + o`).toEqualIgnoringWhitespace("hello"); + }); // Symbol From 2f90f9a4fbf885fb939dd09285deb193243d7653 Mon Sep 17 00:00:00 2001 From: Elad Bezalel Date: Tue, 10 Oct 2023 11:05:05 +0300 Subject: [PATCH 2/2] equality check in matcher & incorrect arg error --- src/bun.js/test/expect.zig | 38 ++++++++++++++++++++++---- src/string_immutable.zig | 12 -------- test/js/bun/test/expect.test.js | 9 ++++++ test/js/bun/test/jest-extended.test.js | 7 +++++ 4 files changed, 48 insertions(+), 18 deletions(-) diff --git a/src/bun.js/test/expect.zig b/src/bun.js/test/expect.zig index 2955955a20e861..ff58965f5ee4f5 100644 --- a/src/bun.js/test/expect.zig +++ b/src/bun.js/test/expect.zig @@ -2666,19 +2666,45 @@ pub const Expect = struct { const expected = arguments[0]; const value: JSValue = this.getValue(globalThis, thisValue, "toEqualIgnoringWhitespace", "expected") orelse return .zero; + if (!expected.isString()) { + globalThis.throw("toEqualIgnoringWhitespace() requires argument to be a string", .{}); + return .zero; + } + const not = this.flags.not; var pass = value.isString() and expected.isString(); if (pass) { - var expectedStr = expected.toString(globalThis).toSlice(globalThis, default_allocator).slice(); var valueStr = value.toString(globalThis).toSlice(globalThis, default_allocator).slice(); + var expectedStr = expected.toString(globalThis).toSlice(globalThis, default_allocator).slice(); + + var left: usize = 0; + var right: usize = 0; + + // Skip leading whitespaces + while (left < valueStr.len and std.ascii.isWhitespace(valueStr[left])) left += 1; + while (right < expectedStr.len and std.ascii.isWhitespace(expectedStr[right])) right += 1; - // Remove all whitespace from both strings - expectedStr = strings.removeWhitespace(default_allocator, expectedStr); - valueStr = strings.removeWhitespace(default_allocator, valueStr); + while (left < valueStr.len and right < expectedStr.len) { + const left_char = valueStr[left]; + const right_char = expectedStr[right]; - // Compare the strings - pass = strings.eql(expectedStr, valueStr); + if (left_char != right_char) { + pass = false; + break; + } + + left += 1; + right += 1; + + // Skip trailing whitespaces + while (left < valueStr.len and std.ascii.isWhitespace(valueStr[left])) left += 1; + while (right < expectedStr.len and std.ascii.isWhitespace(expectedStr[right])) right += 1; + } + + if (left < valueStr.len or right < expectedStr.len) { + pass = false; + } } if (not) pass = !pass; diff --git a/src/string_immutable.zig b/src/string_immutable.zig index 02480aeb552c6d..00ee8d8355a590 100644 --- a/src/string_immutable.zig +++ b/src/string_immutable.zig @@ -4847,15 +4847,3 @@ pub fn concatIfNeeded( } std.debug.assert(remain.len == 0); } - -pub fn removeWhitespace(allocator: std.mem.Allocator, str: []const u8) []u8 { - var result = allocator.alloc(u8, str.len) catch unreachable; - var j: usize = 0; - for (str) |char| { - if (!std.ascii.isWhitespace(char)) { - result[j] = char; - j += 1; - } - } - return result[0..j]; -} diff --git a/test/js/bun/test/expect.test.js b/test/js/bun/test/expect.test.js index 3483ed49a5f005..13b549179a120c 100644 --- a/test/js/bun/test/expect.test.js +++ b/test/js/bun/test/expect.test.js @@ -3090,6 +3090,15 @@ describe("expect()", () => { l l o`).toEqualIgnoringWhitespace("hello"); + expect(`Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec nec posuere felis. Aliquam tincidunt elit a nunc hendrerit maximus. Morbi semper tristique lectus, eget ullamcorper velit ullamcorper non. Aenean nibh augue, ultrices id ornare quis, eleifend id metus. Aliquam erat volutpat. Proin maximus, ligula at consequat venenatis, sapien odio auctor mi, sit amet placerat augue odio et orci. Vivamus tempus hendrerit tortor, et interdum est semper malesuada. Ut venenatis iaculis felis eget euismod. Suspendisse sed nisi eget massa fringilla rhoncus non quis enim. Mauris feugiat pellentesque justo, at sagittis augue sollicitudin vel. Pellentesque porttitor consequat mi nec varius. Praesent aliquet at justo nec finibus. Donec ut lorem eu ex dignissim pulvinar at sit amet sem. Ut fringilla sit amet dolor vitae convallis. Ut faucibus a purus sit amet fermentum. + Sed sit amet tortor magna. Pellentesque laoreet lorem at pulvinar efficitur. Nulla dictum nibh ac gravida semper. Duis tempus elit in ipsum feugiat porttitor.`).toEqualIgnoringWhitespace( + `Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec nec posuere felis. Aliquam tincidunt elit a nunc hendrerit maximus. Morbi semper tristique lectus, eget ullamcorper velit ullamcorper non. Aenean nibh augue, ultrices id ornare quis, eleifend id metus. Aliquam erat volutpat. Proin maximus, ligula at consequat venenatis, sapien odio auctor mi, sit amet placerat augue odio et orci. Vivamus tempus hendrerit tortor, et interdum est semper malesuada. Ut venenatis iaculis felis eget euismod. Suspendisse sed nisi eget massa fringilla rhoncus non quis enim. Mauris feugiat pellentesque justo, at sagittis augue sollicitudin vel. Pellentesque porttitor consequat mi nec varius. Praesent aliquet at justo nec finibus. Donec ut lorem eu ex dignissim pulvinar at sit amet sem. Ut fringilla sit amet dolor vitae convallis. Ut faucibus a purus sit amet fermentum. Sed sit amet tortor magna. Pellentesque laoreet lorem at pulvinar efficitur. Nulla dictum nibh ac gravida semper. Duis tempus elit in ipsum feugiat porttitor.`, + ); + + expect("hello world").not.toEqualIgnoringWhitespace("hello world!"); + expect(() => { + expect({}).not.toEqualIgnoringWhitespace({}); + }).toThrow(); }); test("toBeSymbol()", () => { diff --git a/test/js/bun/test/jest-extended.test.js b/test/js/bun/test/jest-extended.test.js index 6f3d20fa960533..2194a83999738d 100644 --- a/test/js/bun/test/jest-extended.test.js +++ b/test/js/bun/test/jest-extended.test.js @@ -597,6 +597,13 @@ describe("jest-extended", () => { l l o`).toEqualIgnoringWhitespace("hello"); + expect(`Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec nec posuere felis. Aliquam tincidunt elit a nunc hendrerit maximus. Morbi semper tristique lectus, eget ullamcorper velit ullamcorper non. Aenean nibh augue, ultrices id ornare quis, eleifend id metus. Aliquam erat volutpat. Proin maximus, ligula at consequat venenatis, sapien odio auctor mi, sit amet placerat augue odio et orci. Vivamus tempus hendrerit tortor, et interdum est semper malesuada. Ut venenatis iaculis felis eget euismod. Suspendisse sed nisi eget massa fringilla rhoncus non quis enim. Mauris feugiat pellentesque justo, at sagittis augue sollicitudin vel. Pellentesque porttitor consequat mi nec varius. Praesent aliquet at justo nec finibus. Donec ut lorem eu ex dignissim pulvinar at sit amet sem. Ut fringilla sit amet dolor vitae convallis. Ut faucibus a purus sit amet fermentum. + Sed sit amet tortor magna. Pellentesque laoreet lorem at pulvinar efficitur. Nulla dictum nibh ac gravida semper. Duis tempus elit in ipsum feugiat porttitor.`).toEqualIgnoringWhitespace( + `Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec nec posuere felis. Aliquam tincidunt elit a nunc hendrerit maximus. Morbi semper tristique lectus, eget ullamcorper velit ullamcorper non. Aenean nibh augue, ultrices id ornare quis, eleifend id metus. Aliquam erat volutpat. Proin maximus, ligula at consequat venenatis, sapien odio auctor mi, sit amet placerat augue odio et orci. Vivamus tempus hendrerit tortor, et interdum est semper malesuada. Ut venenatis iaculis felis eget euismod. Suspendisse sed nisi eget massa fringilla rhoncus non quis enim. Mauris feugiat pellentesque justo, at sagittis augue sollicitudin vel. Pellentesque porttitor consequat mi nec varius. Praesent aliquet at justo nec finibus. Donec ut lorem eu ex dignissim pulvinar at sit amet sem. Ut fringilla sit amet dolor vitae convallis. Ut faucibus a purus sit amet fermentum. Sed sit amet tortor magna. Pellentesque laoreet lorem at pulvinar efficitur. Nulla dictum nibh ac gravida semper. Duis tempus elit in ipsum feugiat porttitor.`, + ); + + expect("hello world").not.toEqualIgnoringWhitespace("hello world!"); + expect({}).not.toEqualIgnoringWhitespace({}); }); // Symbol