Skip to content

Commit ed1e0c2

Browse files
authored
Make more Locators work with tricky characters (#2409)
- Consistently normalize whitespace of strings - Support a wider set of characters for case-insensitive text matching
1 parent 93348b1 commit ed1e0c2

1 file changed

Lines changed: 64 additions & 13 deletions

File tree

src/org/labkey/test/Locator.java

Lines changed: 64 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,10 @@
4646
import java.util.ArrayList;
4747
import java.util.Arrays;
4848
import java.util.Collections;
49+
import java.util.HashMap;
4950
import java.util.Iterator;
5051
import java.util.List;
52+
import java.util.Map;
5153
import java.util.Objects;
5254
import java.util.Optional;
5355
import 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

Comments
 (0)