From a47f7b1c95e89f46ad066de9f2e147cd6b9b32ea Mon Sep 17 00:00:00 2001 From: Stefan Ludwig Date: Wed, 12 Oct 2016 16:34:21 +0200 Subject: [PATCH] Added escaping of values when using IdStartsWith and IdEndsWith ByProducers. This is necessary because we are using a CSS Selector and there are issues with special characters which leads to strange behavior. --- .../identification/CssSelectorUtils.java | 44 +++++++++++++++ .../identification/producers/CssSelector.java | 2 + .../identification/producers/IdEndsWith.java | 9 +++- .../producers/IdStartsWith.java | 9 +++- .../identification/ByProducersTest.java | 4 +- .../identification/CssSelectorUtilsTest.java | 53 +++++++++++++++++++ .../producers/IdEndsWithTest.java | 2 +- .../producers/IdStartsWithTest.java | 2 +- .../IdentificationIntegrationTest.java | 4 +- .../identification/by-producers.html | 4 +- 10 files changed, 121 insertions(+), 12 deletions(-) create mode 100644 webtester-core/src/main/java/info/novatec/testit/webtester/pagefragments/identification/CssSelectorUtils.java create mode 100644 webtester-core/src/test/java/info/novatec/testit/webtester/pagefragments/identification/CssSelectorUtilsTest.java diff --git a/webtester-core/src/main/java/info/novatec/testit/webtester/pagefragments/identification/CssSelectorUtils.java b/webtester-core/src/main/java/info/novatec/testit/webtester/pagefragments/identification/CssSelectorUtils.java new file mode 100644 index 00000000..5bd9cbe9 --- /dev/null +++ b/webtester-core/src/main/java/info/novatec/testit/webtester/pagefragments/identification/CssSelectorUtils.java @@ -0,0 +1,44 @@ +package info.novatec.testit.webtester.pagefragments.identification; + +import java.util.Arrays; +import java.util.Set; +import java.util.stream.Collectors; + + +/** + * This class includes utility methods for working with CSS Selectors. + * + * @since 2.0.4 + */ +public final class CssSelectorUtils { + + /** These are all special characters in CSS who need escaping. */ + private static final Character[] SPECIAL_CHARS = + { '!', '"', '#', '$', '%', '&', '\'', '(', ')', '*', '+', ',', '-', '.', '/', ':', ';', '<', '=', '>', '?', '@', '[', + '\\', ']', '^', '`', '{', '|', '}', '~' }; + /** See {@link #SPECIAL_CHARS}. */ + private static final Set SPECIAL_CHARS_SET = Arrays.stream(SPECIAL_CHARS).collect(Collectors.toSet()); + + /** + * Escape the given value by prefixing all {@link #SPECIAL_CHARS} with {@code \}. + *

+ * This is necessary for all values used by CSS Selectors. For example when matching on the value of an attribute. + * + * @since 2.0.4 + */ + public static String escape(String value) { + StringBuilder escapedValue = new StringBuilder(value.length() * 2); + for (Character c : value.toCharArray()) { + if (SPECIAL_CHARS_SET.contains(c)) { + escapedValue.append('\\'); + } + escapedValue.append(c); + } + return escapedValue.toString(); + } + + private CssSelectorUtils() { + // utility class + } + +} diff --git a/webtester-core/src/main/java/info/novatec/testit/webtester/pagefragments/identification/producers/CssSelector.java b/webtester-core/src/main/java/info/novatec/testit/webtester/pagefragments/identification/producers/CssSelector.java index b26971bb..07b4baa0 100644 --- a/webtester-core/src/main/java/info/novatec/testit/webtester/pagefragments/identification/producers/CssSelector.java +++ b/webtester-core/src/main/java/info/novatec/testit/webtester/pagefragments/identification/producers/CssSelector.java @@ -7,6 +7,8 @@ /** * This {@link ByProducer} produces a {@link By} using {@link By#cssSelector(String)}. + *

+ * Important: Don't forget to escape special characters when using this selector! * * @see ByProducer * @since 2.0 diff --git a/webtester-core/src/main/java/info/novatec/testit/webtester/pagefragments/identification/producers/IdEndsWith.java b/webtester-core/src/main/java/info/novatec/testit/webtester/pagefragments/identification/producers/IdEndsWith.java index 8618016b..8b97200d 100644 --- a/webtester-core/src/main/java/info/novatec/testit/webtester/pagefragments/identification/producers/IdEndsWith.java +++ b/webtester-core/src/main/java/info/novatec/testit/webtester/pagefragments/identification/producers/IdEndsWith.java @@ -3,11 +3,15 @@ import org.openqa.selenium.By; import info.novatec.testit.webtester.pagefragments.identification.ByProducer; +import info.novatec.testit.webtester.pagefragments.identification.CssSelectorUtils; /** * This {@link ByProducer} produces a {@link By} using {@link By#cssSelector(String)} to partially match an ID - * (ends-with). + * (ends-with). The given value will be escaped using {@link CssSelectorUtils#escape(String)} in order to prevent special + * characters from interfering with the selection. + *

+ * Example: {@code :form:table:selection[5]} will be escaped to {@code \:form\:table\:selection\[5\]} * * @see ByProducer * @since 2.0 @@ -18,7 +22,8 @@ public class IdEndsWith implements ByProducer { @Override public By createBy(String value) { - return By.cssSelector(String.format(ID_ENDS_WITH_PATTERN, value)); + String escapedValue = CssSelectorUtils.escape(value); + return By.cssSelector(String.format(ID_ENDS_WITH_PATTERN, escapedValue)); } @Override diff --git a/webtester-core/src/main/java/info/novatec/testit/webtester/pagefragments/identification/producers/IdStartsWith.java b/webtester-core/src/main/java/info/novatec/testit/webtester/pagefragments/identification/producers/IdStartsWith.java index 1f7fdc5d..dcf3ead4 100644 --- a/webtester-core/src/main/java/info/novatec/testit/webtester/pagefragments/identification/producers/IdStartsWith.java +++ b/webtester-core/src/main/java/info/novatec/testit/webtester/pagefragments/identification/producers/IdStartsWith.java @@ -3,11 +3,15 @@ import org.openqa.selenium.By; import info.novatec.testit.webtester.pagefragments.identification.ByProducer; +import info.novatec.testit.webtester.pagefragments.identification.CssSelectorUtils; /** * This {@link ByProducer} produces a {@link By} using {@link By#cssSelector(String)} to partially match an ID - * (starts-with). + * (starts-with). The given value will be escaped using {@link CssSelectorUtils#escape(String)} in order to prevent special + * characters from interfering with the selection. + *

+ * Example: {@code form:table:selection[5]} will be escaped to {@code form\:table\:selection\[5\]} * * @see ByProducer * @since 2.0 @@ -18,7 +22,8 @@ public class IdStartsWith implements ByProducer { @Override public By createBy(String value) { - return By.cssSelector(String.format(ID_STARTS_WITH_PATTERN, value)); + String escapedValue = CssSelectorUtils.escape(value); + return By.cssSelector(String.format(ID_STARTS_WITH_PATTERN, escapedValue)); } @Override diff --git a/webtester-core/src/test/java/info/novatec/testit/webtester/pagefragments/identification/ByProducersTest.java b/webtester-core/src/test/java/info/novatec/testit/webtester/pagefragments/identification/ByProducersTest.java index 62c01247..01090688 100644 --- a/webtester-core/src/test/java/info/novatec/testit/webtester/pagefragments/identification/ByProducersTest.java +++ b/webtester-core/src/test/java/info/novatec/testit/webtester/pagefragments/identification/ByProducersTest.java @@ -45,7 +45,7 @@ public static class IdEndsWithTest { public void producesCorrectBy() { By by = ByProducers.idEndsWith(":partial-id"); assertThat(by).isInstanceOf(By.ByCssSelector.class); - assertThat(by).hasToString("By.cssSelector: [id$=':partial-id']"); + assertThat(by).hasToString("By.cssSelector: [id$='\\:partial\\-id']"); } } @@ -56,7 +56,7 @@ public static class IdStartsWithTest { public void producesCorrectBy() { By by = ByProducers.idStartsWith("partial-id:"); assertThat(by).isInstanceOf(By.ByCssSelector.class); - assertThat(by).hasToString("By.cssSelector: [id^='partial-id:']"); + assertThat(by).hasToString("By.cssSelector: [id^='partial\\-id\\:']"); } } diff --git a/webtester-core/src/test/java/info/novatec/testit/webtester/pagefragments/identification/CssSelectorUtilsTest.java b/webtester-core/src/test/java/info/novatec/testit/webtester/pagefragments/identification/CssSelectorUtilsTest.java new file mode 100644 index 00000000..732d3a0e --- /dev/null +++ b/webtester-core/src/test/java/info/novatec/testit/webtester/pagefragments/identification/CssSelectorUtilsTest.java @@ -0,0 +1,53 @@ +package info.novatec.testit.webtester.pagefragments.identification; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.assertj.core.api.SoftAssertions; +import org.junit.Test; + + +public class CssSelectorUtilsTest { + + @Test + public void realisticExample() { + assertThat(CssSelectorUtils.escape("table:form:selection[0]")).isEqualTo("table\\:form\\:selection\\[0\\]"); + } + + @Test + public void allRelevantSpecialCharactersAreEscaped() { + SoftAssertions softly = new SoftAssertions(); + softly.assertThat(CssSelectorUtils.escape("!")).isEqualTo("\\!"); + softly.assertThat(CssSelectorUtils.escape("\"")).isEqualTo("\\\""); + softly.assertThat(CssSelectorUtils.escape("#")).isEqualTo("\\#"); + softly.assertThat(CssSelectorUtils.escape("$")).isEqualTo("\\$"); + softly.assertThat(CssSelectorUtils.escape("%")).isEqualTo("\\%"); + softly.assertThat(CssSelectorUtils.escape("&")).isEqualTo("\\&"); + softly.assertThat(CssSelectorUtils.escape("'")).isEqualTo("\\'"); + softly.assertThat(CssSelectorUtils.escape("(")).isEqualTo("\\("); + softly.assertThat(CssSelectorUtils.escape(")")).isEqualTo("\\)"); + softly.assertThat(CssSelectorUtils.escape("*")).isEqualTo("\\*"); + softly.assertThat(CssSelectorUtils.escape("+")).isEqualTo("\\+"); + softly.assertThat(CssSelectorUtils.escape(",")).isEqualTo("\\,"); + softly.assertThat(CssSelectorUtils.escape("-")).isEqualTo("\\-"); + softly.assertThat(CssSelectorUtils.escape(".")).isEqualTo("\\."); + softly.assertThat(CssSelectorUtils.escape("/")).isEqualTo("\\/"); + softly.assertThat(CssSelectorUtils.escape(":")).isEqualTo("\\:"); + softly.assertThat(CssSelectorUtils.escape(";")).isEqualTo("\\;"); + softly.assertThat(CssSelectorUtils.escape("<")).isEqualTo("\\<"); + softly.assertThat(CssSelectorUtils.escape("=")).isEqualTo("\\="); + softly.assertThat(CssSelectorUtils.escape(">")).isEqualTo("\\>"); + softly.assertThat(CssSelectorUtils.escape("?")).isEqualTo("\\?"); + softly.assertThat(CssSelectorUtils.escape("@")).isEqualTo("\\@"); + softly.assertThat(CssSelectorUtils.escape("[")).isEqualTo("\\["); + softly.assertThat(CssSelectorUtils.escape("\\")).isEqualTo("\\\\"); + softly.assertThat(CssSelectorUtils.escape("]")).isEqualTo("\\]"); + softly.assertThat(CssSelectorUtils.escape("^")).isEqualTo("\\^"); + softly.assertThat(CssSelectorUtils.escape("`")).isEqualTo("\\`"); + softly.assertThat(CssSelectorUtils.escape("{")).isEqualTo("\\{"); + softly.assertThat(CssSelectorUtils.escape("|")).isEqualTo("\\|"); + softly.assertThat(CssSelectorUtils.escape("}")).isEqualTo("\\}"); + softly.assertThat(CssSelectorUtils.escape("~")).isEqualTo("\\~"); + softly.assertAll(); + } + +} diff --git a/webtester-core/src/test/java/info/novatec/testit/webtester/pagefragments/identification/producers/IdEndsWithTest.java b/webtester-core/src/test/java/info/novatec/testit/webtester/pagefragments/identification/producers/IdEndsWithTest.java index 1c01a704..c916e2f9 100644 --- a/webtester-core/src/test/java/info/novatec/testit/webtester/pagefragments/identification/producers/IdEndsWithTest.java +++ b/webtester-core/src/test/java/info/novatec/testit/webtester/pagefragments/identification/producers/IdEndsWithTest.java @@ -14,7 +14,7 @@ public class IdEndsWithTest { public void producesCorrectBy() { By by = cut.createBy(":partial-id"); assertThat(by).isInstanceOf(By.ByCssSelector.class); - assertThat(by).hasToString("By.cssSelector: [id$=':partial-id']"); + assertThat(by).hasToString("By.cssSelector: [id$='\\:partial\\-id']"); } @Test diff --git a/webtester-core/src/test/java/info/novatec/testit/webtester/pagefragments/identification/producers/IdStartsWithTest.java b/webtester-core/src/test/java/info/novatec/testit/webtester/pagefragments/identification/producers/IdStartsWithTest.java index 04b7d0b6..00a8f9fd 100644 --- a/webtester-core/src/test/java/info/novatec/testit/webtester/pagefragments/identification/producers/IdStartsWithTest.java +++ b/webtester-core/src/test/java/info/novatec/testit/webtester/pagefragments/identification/producers/IdStartsWithTest.java @@ -14,7 +14,7 @@ public class IdStartsWithTest { public void producesCorrectBy() { By by = cut.createBy("partial-id:"); assertThat(by).isInstanceOf(By.ByCssSelector.class); - assertThat(by).hasToString("By.cssSelector: [id^='partial-id:']"); + assertThat(by).hasToString("By.cssSelector: [id^='partial\\-id\\:']"); } @Test diff --git a/webtester-core/src/test/java/integration/pagefragments/identification/IdentificationIntegrationTest.java b/webtester-core/src/test/java/integration/pagefragments/identification/IdentificationIntegrationTest.java index d2d77591..9c66199d 100644 --- a/webtester-core/src/test/java/integration/pagefragments/identification/IdentificationIntegrationTest.java +++ b/webtester-core/src/test/java/integration/pagefragments/identification/IdentificationIntegrationTest.java @@ -90,9 +90,9 @@ public interface TestPage extends Page { @IdentifyUsing(value = "id", how = Id.class) TextField byId(); - @IdentifyUsing(value = "prefix-", how = IdStartsWith.class) + @IdentifyUsing(value = "prefix:", how = IdStartsWith.class) TextField byIdStartsWith(); - @IdentifyUsing(value = "-suffix", how = IdEndsWith.class) + @IdentifyUsing(value = ":suffix", how = IdEndsWith.class) TextField byIdEndsWith(); @IdentifyUsing(value = "//div[@id='xpath']/input", how = XPath.class) TextField byXpath(); diff --git a/webtester-core/src/test/resources/html/pagefragments/identification/by-producers.html b/webtester-core/src/test/resources/html/pagefragments/identification/by-producers.html index d3213e17..d0f74b74 100644 --- a/webtester-core/src/test/resources/html/pagefragments/identification/by-producers.html +++ b/webtester-core/src/test/resources/html/pagefragments/identification/by-producers.html @@ -19,11 +19,11 @@

This page contains elements for testing the ByProducer implementation of Web ID_STARTS_WITH - + ID_ENDS_WITH - + XPATH