Repository: assertj/assertj (Java)

Issue:
isEqualToNormalizingWhitespace doesn't normalise non breaking space characters
**Describe the bug**
isEqualToNormalizingWhitespace doesn't normalise non breaking space characters

* assertj core 3.21:
* java version: 17
* Kotlin 1.9.0
* test framework version: JUnit 5.9.1
* os (if relevant): 

**Compare 2 strings with one having a [NBSP](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.text/-typography/nbsp.html) character instead of a normal space**

```Kotlin
val result = "10,00 € to 100NBSP000,00 € per year"
val expected = "10,00 € to 100 000,00 € per year"
assertThat(result).isEqualToNormalizingWhitespace(expected)
```



Hints:
That's a bit weird since the [implementation](https://github.com/assertj/assertj/blob/776b46c514c4336c58a643fb08bf61a1ac6cb9b3/assertj-core/src/main/java/org/assertj/core/internal/Strings.java#L455) detects the whitespace characters according to the JDK `Character` method [isWhitespace](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/Character.html#isWhitespace(char)) that includes non-breaking space characters.

Could you attach a test case in a PR, reason is a test in a github comment may or may not honor the non-breaking space characters when we copy it to our codebase.

Thanks!

@joel-costigliola the documentation of the `isWhitespace` method seems to indicate that the non-breaking spaces are  not consider as regular whitespaces.

I've added this test case locally that seems to confirm that:

`should_pass_if_both_Strings_are_equal_after_whitespace_is_normalized`

![image](https://github.com/assertj/assertj/assets/27780296/fd328e59-6e16-4b9b-b625-938321c71193)

I've also noticed that the `isNotBlank `assertion is `false` if a string is filled with non-breaking spaces because it uses the `isWhitespace `method as well (I think that's the expected behavior because some unit tests have been specifically added).

I think that the non-breaking spaces `('\u00A0', '\u2007', '\u202F')` should be replaced in the `normalizeWhitespace` method. 

Is it ok if I work on this issue ? I think this might be a good first issue 🙂 
 

Patch:
diff --git a/assertj-core/src/main/java/org/assertj/core/api/AbstractCharSequenceAssert.java b/assertj-core/src/main/java/org/assertj/core/api/AbstractCharSequenceAssert.java
--- a/assertj-core/src/main/java/org/assertj/core/api/AbstractCharSequenceAssert.java
+++ b/assertj-core/src/main/java/org/assertj/core/api/AbstractCharSequenceAssert.java
@@ -1622,7 +1622,7 @@ public SELF isNotEqualToIgnoringWhitespace(CharSequence expected) {
    * To be exact, the following rules are applied:
    * <ul>
    * <li>all leading and trailing whitespace of both actual and expected strings are ignored</li>
-   * <li>any remaining whitespace, appearing within either string, is collapsed to a single space before comparison</li>
+   * <li>any remaining whitespace (including non-breaking spaces), appearing within either string, is collapsed to a single space before comparison</li>
    * </ul>
    * <p>
    * Example:
@@ -1634,6 +1634,7 @@ public SELF isNotEqualToIgnoringWhitespace(CharSequence expected) {
    *                              .isEqualToNormalizingWhitespace("  Game of   Thrones  ")
    *                              .isEqualToNormalizingWhitespace("Game of\tThrones")
    *                              .isEqualToNormalizingWhitespace("Game of Thrones");
+   *                              .isEqualToNormalizingWhitespace("Game\u00A0of Thrones");
    *
    * // assertions will fail
    * assertThat("Game of Thrones").isEqualToNormalizingWhitespace("Game ofThrones");
@@ -1657,7 +1658,7 @@ public SELF isEqualToNormalizingWhitespace(CharSequence expected) {
    * To be exact, the following rules are applied:
    * <ul>
    * <li>all leading and trailing whitespace of both actual and expected strings are ignored</li>
-   * <li>any remaining whitespace, appearing within either string, is collapsed to a single space before comparison</li>
+   * <li>any remaining whitespace (including non-breaking spaces), appearing within either string, is collapsed to a single space before comparison</li>
    * </ul>
    * <p>
    * Example:
@@ -1686,11 +1687,11 @@ public SELF isNotEqualToNormalizingWhitespace(CharSequence expected) {
 
   /**
    * Verifies that the actual {@code CharSequence} is equal to the given one, after the punctuation
-   * of both strings have been normalized.
+   * of both strings has been normalized.
    * <p>
    * To be exact, the following rules are applied:
    * <ul>
-   * <li>All punctuation of actual and expected strings are ignored and whitespaces are normalized</li>
+   * <li>All punctuation of actual and expected strings are ignored and whitespaces (including non-breaking spaces) are normalized</li>
    * <li>Punctuation is any of the following character <b>{@code !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~}</b></li>
    * </ul>
    * <p>
diff --git a/assertj-core/src/main/java/org/assertj/core/internal/Strings.java b/assertj-core/src/main/java/org/assertj/core/internal/Strings.java
--- a/assertj-core/src/main/java/org/assertj/core/internal/Strings.java
+++ b/assertj-core/src/main/java/org/assertj/core/internal/Strings.java
@@ -84,8 +84,10 @@
 import java.io.StringReader;
 import java.text.Normalizer;
 import java.util.Base64;
+import java.util.Collections;
 import java.util.Comparator;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.LinkedHashMap;
 import java.util.LinkedHashSet;
 import java.util.Map;
@@ -106,6 +108,16 @@
  */
 public class Strings {
 
+  private static final Set<Character> NON_BREAKING_SPACES;
+
+  static {
+    Set<Character> nonBreakingSpaces = new HashSet<>();
+    nonBreakingSpaces.add('\u00A0');
+    nonBreakingSpaces.add('\u2007');
+    nonBreakingSpaces.add('\u202F');
+    NON_BREAKING_SPACES = Collections.unmodifiableSet(nonBreakingSpaces);
+  }
+
   private static final String EMPTY_STRING = "";
   private static final Strings INSTANCE = new Strings();
   private static final String PUNCTUATION_REGEX = "\\p{Punct}";
@@ -385,7 +397,7 @@ private static String normalizeWhitespace(CharSequence toNormalize) {
     boolean lastWasSpace = true;
     for (int i = 0; i < toNormalize.length(); i++) {
       char c = toNormalize.charAt(i);
-      if (isWhitespace(c)) {
+      if (isWhitespace(c) || NON_BREAKING_SPACES.contains(c)) {
         if (!lastWasSpace) result.append(' ');
         lastWasSpace = true;
       } else {


Test patch:
diff --git a/assertj-core/src/test/java/org/assertj/core/api/CharSequenceAssertBaseTest.java b/assertj-core/src/test/java/org/assertj/core/api/CharSequenceAssertBaseTest.java
--- a/assertj-core/src/test/java/org/assertj/core/api/CharSequenceAssertBaseTest.java
+++ b/assertj-core/src/test/java/org/assertj/core/api/CharSequenceAssertBaseTest.java
@@ -16,6 +16,10 @@
 
 import org.assertj.core.internal.Strings;
 
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
 /**
  * Base class for {@link CharSequenceAssert} tests.
  * 
@@ -25,6 +29,17 @@
 public abstract class CharSequenceAssertBaseTest extends BaseTestTemplate<CharSequenceAssert, CharSequence> {
   protected Strings strings;
 
+  protected static final Set<Character> NON_BREAKING_SPACES;
+
+  static {
+    Set<Character> nonBreakingSpaces = new HashSet<>();
+    nonBreakingSpaces.add('\u00A0');
+    nonBreakingSpaces.add('\u2007');
+    nonBreakingSpaces.add('\u202F');
+
+    NON_BREAKING_SPACES = Collections.unmodifiableSet(nonBreakingSpaces);
+  }
+
   @Override
   protected CharSequenceAssert create_assertions() {
     return new CharSequenceAssert("Yoda");
diff --git a/assertj-core/src/test/java/org/assertj/core/api/charsequence/CharSequenceAssert_isEqualToNormalizingPunctuationAndWhitespace_Test.java b/assertj-core/src/test/java/org/assertj/core/api/charsequence/CharSequenceAssert_isEqualToNormalizingPunctuationAndWhitespace_Test.java
--- a/assertj-core/src/test/java/org/assertj/core/api/charsequence/CharSequenceAssert_isEqualToNormalizingPunctuationAndWhitespace_Test.java
+++ b/assertj-core/src/test/java/org/assertj/core/api/charsequence/CharSequenceAssert_isEqualToNormalizingPunctuationAndWhitespace_Test.java
@@ -12,10 +12,17 @@
  */
 package org.assertj.core.api.charsequence;
 
-import static org.mockito.Mockito.verify;
-
 import org.assertj.core.api.CharSequenceAssert;
 import org.assertj.core.api.CharSequenceAssertBaseTest;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.util.stream.Stream;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.params.provider.Arguments.arguments;
+import static org.mockito.Mockito.verify;
 
 /**
  * Created by harisha talanki on 2/29/20
@@ -32,4 +39,16 @@ protected void verify_internal_effects() {
     verify(strings).assertEqualsNormalizingPunctuationAndWhitespace(getInfo(assertions), getActual(assertions),
                                                                     "Game of Thrones");
   }
+
+  @ParameterizedTest
+  @MethodSource("notEqualToNormalizingWhiteSpaceGenerator")
+  void should_pass_if_actual_is_equal_normalizing_breaking_spaces(String actual, String expected) {
+    assertThat(actual).isEqualToNormalizingPunctuationAndWhitespace(expected);
+  }
+
+  public static Stream<Arguments> notEqualToNormalizingWhiteSpaceGenerator() {
+    return NON_BREAKING_SPACES.stream()
+                              .map(nonBreakingSpace -> arguments("my" + nonBreakingSpace
+                                                                 + "foo bar", "my foo bar"));
+  }
 }
diff --git a/assertj-core/src/test/java/org/assertj/core/api/charsequence/CharSequenceAssert_isNotEqualToNormalizingWhitespace_Test.java b/assertj-core/src/test/java/org/assertj/core/api/charsequence/CharSequenceAssert_isNotEqualToNormalizingWhitespace_Test.java
--- a/assertj-core/src/test/java/org/assertj/core/api/charsequence/CharSequenceAssert_isNotEqualToNormalizingWhitespace_Test.java
+++ b/assertj-core/src/test/java/org/assertj/core/api/charsequence/CharSequenceAssert_isNotEqualToNormalizingWhitespace_Test.java
@@ -12,10 +12,19 @@
  */
 package org.assertj.core.api.charsequence;
 
-import static org.mockito.Mockito.verify;
-
 import org.assertj.core.api.CharSequenceAssert;
 import org.assertj.core.api.CharSequenceAssertBaseTest;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.util.stream.Stream;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+import static org.assertj.core.error.ShouldNotBeEqualNormalizingWhitespace.shouldNotBeEqualNormalizingWhitespace;
+import static org.junit.jupiter.params.provider.Arguments.arguments;
+import static org.mockito.Mockito.verify;
 
 /**
  * Tests for <code>{@link org.assertj.core.api.CharSequenceAssert#isNotEqualToNormalizingWhitespace(CharSequence)}</code>.
@@ -33,4 +42,19 @@ protected CharSequenceAssert invoke_api_method() {
   protected void verify_internal_effects() {
     verify(strings).assertNotEqualsNormalizingWhitespace(getInfo(assertions), getActual(assertions), " my foo bar ");
   }
+
+  @ParameterizedTest
+  @MethodSource("notEqualToNormalizingWhiteSpaceGenerator")
+  void should_fail_if_actual_is_equal_normalizing_breaking_spaces(String actual, String expected) {
+    assertThatExceptionOfType(AssertionError.class)
+                                                   .isThrownBy(() -> assertThat(actual).isNotEqualToNormalizingWhitespace(expected))
+                                                   .withMessage(shouldNotBeEqualNormalizingWhitespace(actual,
+                                                                                                      expected).create());
+  }
+
+  public static Stream<Arguments> notEqualToNormalizingWhiteSpaceGenerator() {
+    return NON_BREAKING_SPACES.stream()
+                              .map(nonBreakingSpace -> arguments("my" + nonBreakingSpace
+                                                                 + "foo bar", "my foo bar"));
+  }
 }
diff --git a/assertj-core/src/test/java/org/assertj/core/internal/StringsBaseTest.java b/assertj-core/src/test/java/org/assertj/core/internal/StringsBaseTest.java
--- a/assertj-core/src/test/java/org/assertj/core/internal/StringsBaseTest.java
+++ b/assertj-core/src/test/java/org/assertj/core/internal/StringsBaseTest.java
@@ -15,6 +15,10 @@
 import static org.assertj.core.test.TestData.someInfo;
 import static org.mockito.Mockito.spy;
 
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
 import org.assertj.core.api.AssertionInfo;
 import org.assertj.core.test.CaseInsensitiveStringComparator;
 import org.junit.jupiter.api.BeforeEach;
@@ -38,6 +42,17 @@ public class StringsBaseTest {
   protected ComparatorBasedComparisonStrategy comparisonStrategy;
   protected Strings stringsWithCaseInsensitiveComparisonStrategy;
 
+  protected static final Set<Character> NON_BREAKING_SPACES;
+
+  static {
+    Set<Character> nonBreakingSpaces = new HashSet<>();
+    nonBreakingSpaces.add('\u00A0');
+    nonBreakingSpaces.add('\u2007');
+    nonBreakingSpaces.add('\u202F');
+
+    NON_BREAKING_SPACES = Collections.unmodifiableSet(nonBreakingSpaces);
+  }
+
   @BeforeEach
   public void setUp() {
     failures = spy(new Failures());
diff --git a/assertj-core/src/test/java/org/assertj/core/internal/strings/Strings_assertEqualsNormalizingWhitespace_Test.java b/assertj-core/src/test/java/org/assertj/core/internal/strings/Strings_assertEqualsNormalizingWhitespace_Test.java
--- a/assertj-core/src/test/java/org/assertj/core/internal/strings/Strings_assertEqualsNormalizingWhitespace_Test.java
+++ b/assertj-core/src/test/java/org/assertj/core/internal/strings/Strings_assertEqualsNormalizingWhitespace_Test.java
@@ -13,12 +13,14 @@
 package org.assertj.core.internal.strings;
 
 import static java.lang.String.format;
+import static java.util.stream.Stream.concat;
 import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
 import static org.assertj.core.api.Assertions.assertThatNullPointerException;
 import static org.assertj.core.error.ShouldBeEqualNormalizingWhitespace.shouldBeEqualNormalizingWhitespace;
 import static org.assertj.core.internal.ErrorMessages.charSequenceToLookForIsNull;
 import static org.assertj.core.test.CharArrays.arrayOf;
 import static org.assertj.core.test.TestData.someInfo;
+import static org.junit.jupiter.params.provider.Arguments.arguments;
 
 import java.util.stream.Stream;
 
@@ -29,8 +31,6 @@
 import org.junit.jupiter.params.provider.MethodSource;
 
 /**
- * Tests for <code>{@link org.assertj.core.internal.Strings#assertEqualsNormalizingWhitespace(org.assertj.core.api.AssertionInfo, CharSequence, CharSequence)} </code>.
- *
  * @author Alex Ruiz
  * @author Joel Costigliola
  * @author Alexander Bischof
@@ -69,17 +69,26 @@ void should_pass_if_both_Strings_are_equal_after_whitespace_is_normalized(String
     strings.assertEqualsNormalizingWhitespace(someInfo(), actual, expected);
   }
 
-  public static Stream<Arguments> equalNormalizingWhitespaceGenerator() {
-    return Stream.of(Arguments.of("my   foo bar", "my foo bar"),
-                     Arguments.of("  my foo bar  ", "my foo bar"),
-                     Arguments.of(" my\tfoo bar ", " my foo bar"),
-                     Arguments.of(" my foo    bar ", "my foo bar"),
-                     Arguments.of(" my foo    bar ", "  my foo bar   "),
-                     Arguments.of("       ", " "),
-                     Arguments.of(" my\tfoo bar ", new String(arrayOf(' ', 'm', 'y', ' ', 'f', 'o', 'o', ' ', 'b', 'a', 'r'))),
-                     Arguments.of(" my\tfoo bar ", " my\tfoo bar "),   // same
-                     Arguments.of(null, null),   // null
-                     Arguments.of(" \t \t", " "),
-                     Arguments.of(" abc", "abc "));
+  static Stream<Arguments> equalNormalizingWhitespaceGenerator() {
+    Stream<Arguments> regularWhiteSpaces = Stream.of(arguments("my   foo bar", "my foo bar"),
+                                                     arguments("  my foo bar  ", "my foo bar"),
+                                                     arguments(" my\tfoo bar ", " my foo bar"),
+                                                     arguments(" my foo    bar ", "my foo bar"),
+                                                     arguments(" my foo    bar ", "  my foo bar   "),
+                                                     arguments("       ", " "),
+                                                     arguments(" my\tfoo bar ",
+                                                               new String(arrayOf(' ', 'm', 'y', ' ', 'f', 'o', 'o', ' ', 'b',
+                                                                                  'a', 'r'))),
+                                                     arguments(" my\tfoo bar ", " my\tfoo bar "),   // same
+                                                     arguments(null, null),   // null
+                                                     arguments(" \t \t", " "),
+                                                     arguments(" abc", "abc "));
+
+    Stream<Arguments> nonBreakingSpaces = NON_BREAKING_SPACES.stream()
+                                                             .map(nonBreakingSpace -> arguments("my" + nonBreakingSpace
+                                                                                                + "foo bar", "my foo bar"));
+
+    return concat(regularWhiteSpaces, nonBreakingSpaces);
   }
+
 }