4646import java .util .ArrayList ;
4747import java .util .Arrays ;
4848import java .util .Collections ;
49+ import java .util .HashMap ;
4950import java .util .Iterator ;
5051import java .util .List ;
52+ import java .util .Map ;
5153import java .util .Objects ;
5254import java .util .Optional ;
5355import java .util .function .Function ;
@@ -975,6 +977,16 @@ public static String xq(String value)
975977 return Quotes .escape (value );
976978 }
977979
980+ /**
981+ * Equivalent to XPath {@code normalize-space()}:<br>
982+ * "The normalize-space function strips leading and trailing white-space from a string, replaces sequences of
983+ * whitespace characters by a single space, and returns the resulting string."
984+ */
985+ private static String ns (String value )
986+ {
987+ return value .replaceAll ("\\ s+" , " " ).trim ();
988+ }
989+
978990 public static String cq (String value )
979991 {
980992 return "\" " + value .replace ("\\ " , "\\ \\ " ).replace ("\" " , "\\ \" " ) + "\" " ;
@@ -1189,36 +1201,36 @@ protected XPathLocator(@Language("XPath") String loc)
11891201 public XPathLocator containing (String contains )
11901202 {
11911203 if (contains != null && !contains .isEmpty ())
1192- return this .withPredicate ("contains(normalize-space(), " +xq (contains )+")" );
1204+ return this .withPredicate ("contains(normalize-space(), " +xq (ns ( contains ) )+")" );
11931205 else
11941206 return this ;
11951207 }
11961208
11971209 public XPathLocator containingIgnoreCase (String contains )
11981210 {
11991211 if (contains != null && !contains .isEmpty ())
1200- return this .withPredicate ("contains(translate( normalize-space(), 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz') , " +xq (contains .toLowerCase ())+")" );
1212+ return this .withPredicate ("contains(" + toLowerCase ( " normalize-space()" , contains ) + " , " +xq (ns ( contains .toLowerCase () ))+")" );
12011213 else
12021214 return this ;
12031215 }
12041216
12051217 public XPathLocator notContaining (String contains )
12061218 {
1207- return this .withPredicate ("not(contains(normalize-space(), " +xq (contains )+"))" );
1219+ return this .withPredicate ("not(contains(normalize-space(), " +xq (ns ( contains ) )+"))" );
12081220 }
12091221
12101222 public XPathLocator notContainingIgnoreCase (String contains )
12111223 {
12121224 if (contains != null && !contains .isEmpty ())
1213- return this .withPredicate ("not(contains(translate( normalize-space(), 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz') , " +xq (contains .toLowerCase ())+"))" );
1225+ return this .withPredicate ("not(contains(" + toLowerCase ( " normalize-space()" , contains ) + " , " +xq (ns ( contains .toLowerCase () ))+"))" );
12141226 else
12151227 return this ;
12161228 }
12171229
12181230 @ Override
12191231 public XPathLocator withText (String text )
12201232 {
1221- return this .withPredicate ("normalize-space()=" +xq (text ));
1233+ return this .withPredicate ("normalize-space()=" +xq (ns ( text ) ));
12221234 }
12231235
12241236 public XPathLocator withText ()
@@ -1228,7 +1240,7 @@ public XPathLocator withText()
12281240
12291241 public XPathLocator withoutText (String text )
12301242 {
1231- return this .withPredicate ("not(normalize-space()=" + xq (text ) + ")" );
1243+ return this .withPredicate ("not(normalize-space()=" + xq (ns ( text ) ) + ")" );
12321244 }
12331245
12341246 public XPathLocator withoutText ()
@@ -1238,17 +1250,17 @@ public XPathLocator withoutText()
12381250
12391251 public XPathLocator withTextMatching (String regex )
12401252 {
1241- return this .withPredicate ("matches(normalize-space(), " + xq (regex ) + ")" );
1253+ return this .withPredicate ("matches(normalize-space(), " + xq (ns ( regex ) ) + ")" );
12421254 }
12431255
12441256 public XPathLocator startsWith (String text )
12451257 {
1246- return this .withPredicate ("starts-with(normalize-space(), " +xq (text )+")" );
1258+ return this .withPredicate ("starts-with(normalize-space(), " +xq (ns ( text ) )+")" );
12471259 }
12481260
12491261 public XPathLocator startsWithIgnoreCase (String text )
12501262 {
1251- return this .withPredicate ("starts-with(translate( normalize-space(), 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz') , " +xq (text .toLowerCase ())+")" );
1263+ return this .withPredicate ("starts-with(" + toLowerCase ( " normalize-space()" , text ) + " , " +xq (ns ( text .toLowerCase () ))+")" );
12521264 }
12531265
12541266 @ Override
@@ -1404,7 +1416,7 @@ public XPathLocator withAttributeMatchingOtherElementAttribute(String attribute,
14041416
14051417 public XPathLocator endsWith (String substring )
14061418 {
1407- return this .endsWith ("normalize-space()" , substring );
1419+ return this .endsWith ("normalize-space()" , ns ( substring ) );
14081420 }
14091421
14101422 private XPathLocator endsWith (String expression , String substring )
@@ -1451,9 +1463,48 @@ public XPathLocator withAttributeContaining(String attrName, String partialAttrV
14511463
14521464 public XPathLocator withAttributeIgnoreCase (String attrName , String attrVal )
14531465 {
1454- return this .withPredicate (
1455- String .format ("translate(@%s, 'ABCDEFGHIJKLMNOPQRSTUVWXYZÅ', 'abcdefghijklmnopqrstuvwxyzå')=%s" ,
1456- attrName , xq (attrVal .toLowerCase ())));
1466+ return this .withPredicate (toLowerCase ("@" + attrName , attrVal ) + "=" + xq (attrVal .toLowerCase ()));
1467+ }
1468+
1469+ /**
1470+ * Generate an XPath 'translate' statement that will convert the provided statement to lowerCase so that it can
1471+ * be compared to the target value. Allow case-insensitive comparison with values containing tricky characters.
1472+ * The Locator "{@code Locator.tag("span").containingIgnoreCase("test")}", produces an XPath like:
1473+ * "{@code //span[contains(translate(normalize-space(), 'TES', 'tes'), 'test')]}"<br>
1474+ * This would find "{@code <span>test</span}" or "{@code <span>TESTING</span}"
1475+ *
1476+ * @param sourceStatement XPath statement to apply the translation to
1477+ * @param targetValue String that 'sourceStatement' will be compared to
1478+ * @return translate statement to be used in XPath predicate
1479+ */
1480+ private String toLowerCase (String sourceStatement , String targetValue )
1481+ {
1482+ Map <Character , Character > caseMap = new HashMap <>(); // upperCase -> lowerCase
1483+ char [] upperCase = targetValue .toUpperCase ().toCharArray ();
1484+ char [] lowerCase = targetValue .toLowerCase ().toCharArray ();
1485+ for (int i = 0 ; i < targetValue .length (); i ++)
1486+ {
1487+ if (upperCase [i ] != lowerCase [i ])
1488+ {
1489+ caseMap .put (Character .valueOf (upperCase [i ]), Character .valueOf (lowerCase [i ]));
1490+ }
1491+ }
1492+ if (caseMap .isEmpty ())
1493+ {
1494+ return sourceStatement ;
1495+ }
1496+ else
1497+ {
1498+ StringBuilder upperCaseBuilder = new StringBuilder ();
1499+ StringBuilder lowerCaseBuilder = new StringBuilder ();
1500+ caseMap .forEach ((u , l ) -> {
1501+ upperCaseBuilder .append (u );
1502+ lowerCaseBuilder .append (l );
1503+ });
1504+ return "translate(" + sourceStatement + ", " +
1505+ "'" + upperCaseBuilder + "' ," +
1506+ "'" + lowerCaseBuilder + "')" ;
1507+ }
14571508 }
14581509
14591510 public XPathLocator withoutAttribute (String attrName , String attrVal )
0 commit comments