diff --git a/hamster-selenium-component-mat/pom.xml b/hamster-selenium-component-mat/pom.xml index 9ab17423..54451476 100644 --- a/hamster-selenium-component-mat/pom.xml +++ b/hamster-selenium-component-mat/pom.xml @@ -1,4 +1,28 @@ + + hamster-selenium-parent @@ -17,4 +41,4 @@ - \ No newline at end of file + diff --git a/hamster-selenium-component-materialui/src/test/java/com/github/grossopa/selenium/component/mui/v4/core/MuiGridTest.java b/hamster-selenium-component-materialui/src/test/java/com/github/grossopa/selenium/component/mui/v4/core/MuiGridTest.java index 50c388ec..58c853f8 100644 --- a/hamster-selenium-component-materialui/src/test/java/com/github/grossopa/selenium/component/mui/v4/core/MuiGridTest.java +++ b/hamster-selenium-component-materialui/src/test/java/com/github/grossopa/selenium/component/mui/v4/core/MuiGridTest.java @@ -1,3 +1,27 @@ +/* + * Copyright © 2023 the original author or authors. + * + * Licensed under the The MIT License (MIT) (the "License"); + * You may obtain a copy of the License at + * + * https://mit-license.org/ + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software + * and associated documentation files (the “Software”), to deal in the Software without + * restriction, including without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING + * BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + package com.github.grossopa.selenium.component.mui.v4.core; import com.github.grossopa.selenium.component.mui.config.MuiConfig; diff --git a/hamster-selenium-core/src/main/java/com/github/grossopa/selenium/core/locator/By2Builder.java b/hamster-selenium-core/src/main/java/com/github/grossopa/selenium/core/locator/By2Builder.java index e6f7c730..df20a1be 100644 --- a/hamster-selenium-core/src/main/java/com/github/grossopa/selenium/core/locator/By2Builder.java +++ b/hamster-selenium-core/src/main/java/com/github/grossopa/selenium/core/locator/By2Builder.java @@ -33,7 +33,6 @@ * @author Jack Yin * @since 1.5 */ -@FunctionalInterface public interface By2Builder { /** @@ -42,4 +41,11 @@ public interface By2Builder { * @return the built by instance. */ By build(); + + /** + * Gets the XPath string. + * + * @return the xpath string. + */ + String xpathString(); } diff --git a/hamster-selenium-core/src/main/java/com/github/grossopa/selenium/core/locator/OperatorType.java b/hamster-selenium-core/src/main/java/com/github/grossopa/selenium/core/locator/OperatorType.java new file mode 100644 index 00000000..8865fc1f --- /dev/null +++ b/hamster-selenium-core/src/main/java/com/github/grossopa/selenium/core/locator/OperatorType.java @@ -0,0 +1,50 @@ +/* + * Copyright © 2021 the original author or authors. + * + * Licensed under the The MIT License (MIT) (the "License"); + * You may obtain a copy of the License at + * + * https://mit-license.org/ + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software + * and associated documentation files (the “Software”), to deal in the Software without + * restriction, including without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING + * BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.github.grossopa.selenium.core.locator; + +/** + * The and / or logical operator + * + * @author Jack Yin + * @since 1.11 + */ +public enum OperatorType { + AND(" and "), OR(" or "); + + private final String value; + + OperatorType(String value) { + this.value = value; + } + + /** + * Gets the value string of the operator + * + * @return the value string of the operator + */ + public String getValue() { + return value; + } +} diff --git a/hamster-selenium-core/src/main/java/com/github/grossopa/selenium/core/locator/SimpleXpathBuilder.java b/hamster-selenium-core/src/main/java/com/github/grossopa/selenium/core/locator/SimpleXpathBuilder.java index 09dfd848..7a1eb975 100644 --- a/hamster-selenium-core/src/main/java/com/github/grossopa/selenium/core/locator/SimpleXpathBuilder.java +++ b/hamster-selenium-core/src/main/java/com/github/grossopa/selenium/core/locator/SimpleXpathBuilder.java @@ -30,7 +30,7 @@ import static com.github.grossopa.selenium.core.util.SeleniumUtils.enrichQuote; import static java.util.Objects.requireNonNull; -import static org.apache.commons.lang3.StringUtils.defaultIfBlank; +import static org.apache.commons.lang3.StringUtils.*; /** * A simple builder for creating Selenium simple xpath in a readable way. @@ -50,7 +50,7 @@ private SimpleXpathBuilder() { } /** - * Builds the prefix and tag as the first step. + * Builds the xpath and tag as the first step. * * @author Jack Yin * @since 1.5 @@ -58,7 +58,7 @@ private SimpleXpathBuilder() { public static class PrefixBuilder { /** - * Adds the prefix ".//*" to search any depth child. For example: + * Adds the xpath ".//*" to search any depth child. For example: * *

* div (current) -< span -< span -< li (target) @@ -71,7 +71,7 @@ public SourceBuilder anywhereRelative() { } /** - * Adds the prefix ".//tag" to search any depth child. For example: + * Adds the xpath ".//tag" to search any depth child. For example: * *

* div (current) -< span -< span -< li (target) @@ -85,7 +85,7 @@ public SourceBuilder anywhereRelative(@Nullable String tag) { } /** - * Adds the prefix "//*" to search any depth element absolutely. + * Adds the xpath "//*" to search any depth element absolutely. * *

* div (current)
div (another) -< span -< span -< li (target) @@ -98,7 +98,7 @@ public SourceBuilder anywhere() { } /** - * Adds the prefix "//tag" to search any depth element absolutely. + * Adds the xpath "//tag" to search any depth element absolutely. * *

* div (current)
div (another) -< span -< span -< li (target) @@ -112,7 +112,7 @@ public SourceBuilder anywhere(String tag) { } /** - * Adds the prefix "./*" to to search relative elements + * Adds the xpath "./*" to to search relative elements * *

* div (current) -< li (target) @@ -125,7 +125,7 @@ public SourceBuilder relative() { } /** - * Adds the prefix "./tag" to to search relative elements + * Adds the xpath "./tag" to to search relative elements * *

* div (current) -< li (target) @@ -139,7 +139,7 @@ public SourceBuilder relative(String tag) { } /** - * Builds with tag as * and empty prefix. + * Builds with tag as * and empty xpath. * * @return created next builder {@link SourceBuilder} instance */ @@ -148,7 +148,7 @@ public SourceBuilder empty() { } /** - * Builds with tag and empty prefix. + * Builds with tag and empty xpath. * * @param tag the target tag name to search, defaulted to * if blank * @return created next builder {@link SourceBuilder} instance @@ -165,49 +165,68 @@ public SourceBuilder empty(String tag) { * @since 1.5 */ public static class SourceBuilder implements By2Builder { - private final String prefix; + private final String xpath; + @Nullable + private final OperatorType operator; /** - * Constructs an instance with non-null prefix. + * Constructs an instance with non-null xpath. * - * @param prefix the prefix generated by previous {@link PrefixBuilder}. + * @param xpath the xpath generated by previous {@link PrefixBuilder}. */ - SourceBuilder(String prefix) { - this.prefix = prefix; + SourceBuilder(String xpath) { + this(xpath, null); + } + + + /** + * Constructs an instance with non-null xpath. + * + * @param xpath the xpath generated by previous {@link PrefixBuilder}. + * @param operator the and / or operator, could be null + */ + SourceBuilder(String xpath, @Nullable OperatorType operator) { + this.xpath = xpath; + this.operator = operator; } /** * Builds the source as {@code text()} * - * @return created next build {@link MethodBuilder} instance + * @return created next build {@link WithNotMethodBuilder} instance */ - public MethodBuilder text() { - return new MethodBuilder(prefix, "text()"); + public WithNotMethodBuilder text() { + return new WithNotMethodBuilder(xpath, "text()", operator); } /** * Builds the source as {@code name()} * - * @return created next build {@link MethodBuilder} instance + * @return created next build {@link WithNotMethodBuilder} instance */ - public MethodBuilder name() { - return new MethodBuilder(prefix, "name()"); + public WithNotMethodBuilder name() { + return new WithNotMethodBuilder(xpath, "name()", operator); } /** * Builds the source as attribute * * @param attr the attribute to match - * @return created next build {@link MethodBuilder} instance + * @return created next build {@link WithNotMethodBuilder} instance */ - public MethodBuilder attr(String attr) { + public WithNotMethodBuilder attr(String attr) { String source = attr.startsWith("@") ? attr : "@" + attr; - return new MethodBuilder(prefix, source); + return new WithNotMethodBuilder(xpath, source, operator); } @Override public By build() { - return By.xpath(prefix); + return By.xpath(xpath); + } + + @Override + public String xpathString() { + return xpath; } /** @@ -216,32 +235,56 @@ public By build() { * @return the next created {@link AxesBuilder} builder instance */ public AxesBuilder axes() { - return new AxesBuilder(prefix); + return new AxesBuilder(xpath); } } /** - * Builds the method as the last step. Note this is not a {@link By2Builder}. + * Method Builder with not() function. * * @author Jack Yin - * @since 1.5 + * @since 1.11 */ - public static class MethodBuilder { + public static class WithNotMethodBuilder extends MethodBuilder { - private final String prefix; - private final String source; + WithNotMethodBuilder(String xpath, String source, OperatorType operator) { + super(xpath, source, operator); + } /** - * Builds the source as the last step. + * Wraps the next method with not() * - * @author Jack Yin - * @since 1.5 + * @return the created next build {@link MethodBuilder} instance */ - MethodBuilder(String prefix, String source) { - requireNonNull(prefix); + public MethodBuilder not() { + return new MethodBuilder(xpath, source, true, operator); + } + } + + /** + * Builds the method as the last step. Note this is not a {@link By2Builder}. + * + * @author Jack Yin + * @since 1.5 + */ + public static class MethodBuilder { + + protected final String xpath; + protected final String source; + protected final boolean notOperator; + protected final OperatorType operator; + + MethodBuilder(String xpath, String source, @Nullable OperatorType operator) { + this(xpath, source, false, operator); + } + + MethodBuilder(String xpath, String source, boolean notOperator, @Nullable OperatorType operator) { + requireNonNull(xpath); requireNonNull(source); - this.prefix = prefix; + this.xpath = xpath; this.source = source; + this.notOperator = notOperator; + this.operator = operator; } /** @@ -251,7 +294,7 @@ public static class MethodBuilder { * @return the generated by xpath */ public AxesBuilder exact(String term) { - return new AxesBuilder(prefix + String.format("[%s=%s]", source, enrichQuote(term))); + return new AxesBuilder(buildXpath("%s=%s", term)); } /** @@ -261,7 +304,7 @@ public AxesBuilder exact(String term) { * @return the generated by xpath */ public AxesBuilder contains(String term) { - return new AxesBuilder(prefix + String.format("[contains(%s,%s)]", source, enrichQuote(term))); + return new AxesBuilder(buildXpath("contains(%s,%s)", term)); } /** @@ -271,7 +314,7 @@ public AxesBuilder contains(String term) { * @return the generated by xpath */ public AxesBuilder startsWith(String term) { - return new AxesBuilder(prefix + String.format("[starts-with(%s,%s)]", source, enrichQuote(term))); + return new AxesBuilder(buildXpath("starts-with(%s,%s)", term)); } /** @@ -281,22 +324,36 @@ public AxesBuilder startsWith(String term) { * @return the generated by xpath */ public AxesBuilder matches(String patternString) { - return new AxesBuilder(prefix + String.format("[matches(%s,%s)]", source, enrichQuote(patternString))); + return new AxesBuilder(buildXpath("matches(%s,%s)", patternString)); + } + + private String buildXpath(String template, String value) { + if (notOperator) { + template = "not(" + template + ")"; + } + + String formatted = String.format(template, source, enrichQuote(value)); + + if (operator != null) { + return xpath.substring(0, xpath.length() - 1) + operator.getValue() + formatted + "]"; + } else { + return xpath + "[" + formatted + "]"; + } } } public static class AxesBuilder implements By2Builder { - private final String prefix; + private final String xpath; /** * Constructs an instance with previous built xpath. * - * @param prefix the current xpath built in previous step. + * @param xpath the current xpath built in previous step. */ - public AxesBuilder(String prefix) { - requireNonNull(prefix); - this.prefix = prefix; + public AxesBuilder(String xpath) { + requireNonNull(xpath); + this.xpath = xpath; } /** @@ -315,7 +372,7 @@ public SourceBuilder child() { * @return the next created {@link SourceBuilder} builder instance. */ public SourceBuilder child(String tag) { - return new SourceBuilder(prefix + "/child::" + defaultIfBlank(tag, ANY_TAG)); + return new SourceBuilder(xpath + "/child::" + defaultIfBlank(tag, ANY_TAG)); } /** @@ -336,7 +393,7 @@ public SourceBuilder descendant() { * @return the next created {@link SourceBuilder} builder instance. */ public SourceBuilder descendant(String tag) { - return new SourceBuilder(prefix + "/descendant::" + defaultIfBlank(tag, ANY_TAG)); + return new SourceBuilder(xpath + "/descendant::" + defaultIfBlank(tag, ANY_TAG)); } /** @@ -356,7 +413,7 @@ public SourceBuilder parent() { * @return the next created {@link SourceBuilder} builder instance. */ public SourceBuilder parent(String tag) { - return new SourceBuilder(prefix + "/parent::" + defaultIfBlank(tag, ANY_TAG)); + return new SourceBuilder(xpath + "/parent::" + defaultIfBlank(tag, ANY_TAG)); } /** @@ -379,7 +436,7 @@ public SourceBuilder ancestor() { * @return the next created {@link SourceBuilder} instance. */ public SourceBuilder ancestor(String tag) { - return new SourceBuilder(prefix + "/ancestor::" + defaultIfBlank(tag, ANY_TAG)); + return new SourceBuilder(xpath + "/ancestor::" + defaultIfBlank(tag, ANY_TAG)); } /** @@ -400,7 +457,7 @@ public SourceBuilder precedingSibling() { * @return the next created {@link SourceBuilder} instance. */ public SourceBuilder precedingSibling(String tag) { - return new SourceBuilder(prefix + "/preceding-sibling::" + defaultIfBlank(tag, ANY_TAG)); + return new SourceBuilder(xpath + "/preceding-sibling::" + defaultIfBlank(tag, ANY_TAG)); } /** @@ -421,7 +478,7 @@ public SourceBuilder followingSibling() { * @return the next created {@link SourceBuilder} instance. */ public SourceBuilder followingSibling(String tag) { - return new SourceBuilder(prefix + "/following-sibling::" + defaultIfBlank(tag, ANY_TAG)); + return new SourceBuilder(xpath + "/following-sibling::" + defaultIfBlank(tag, ANY_TAG)); } /** @@ -442,7 +499,7 @@ public SourceBuilder following() { * @return the next created {@link SourceBuilder} instance. */ public SourceBuilder following(String tag) { - return new SourceBuilder(prefix + "/following::" + defaultIfBlank(tag, ANY_TAG)); + return new SourceBuilder(xpath + "/following::" + defaultIfBlank(tag, ANY_TAG)); } /** @@ -463,12 +520,37 @@ public SourceBuilder preceding() { * @return the next created {@link SourceBuilder} instance. */ public SourceBuilder preceding(String tag) { - return new SourceBuilder(prefix + "/preceding::" + defaultIfBlank(tag, ANY_TAG)); + return new SourceBuilder(xpath + "/preceding::" + defaultIfBlank(tag, ANY_TAG)); + } + + /** + * Insert next expression with and logical operator. [@value="aa" and contains(text(), "bb")] + * + * @return the next created {@link SourceBuilder} instance with {@link OperatorType#AND} operator. + */ + public SourceBuilder and() { + return new SourceBuilder(xpath, OperatorType.AND); + } + + /** + * Insert next expression with and logical operator. [@value="aa" or contains(text(), "bb")] + * + * @return the next created {@link SourceBuilder} instance with {@link OperatorType#OR} operator. + */ + public SourceBuilder or() { + return new SourceBuilder(xpath, OperatorType.OR); } @Override public By build() { - return By.xpath(prefix); + return By.xpath(xpath); } + + @Override + public String xpathString() { + return xpath; + } + + } } diff --git a/hamster-selenium-core/src/test/java/com/github/grossopa/selenium/core/locator/SimpleXpathBuilderTest.java b/hamster-selenium-core/src/test/java/com/github/grossopa/selenium/core/locator/SimpleXpathBuilderTest.java index 2a0ffa8b..426b5aa6 100644 --- a/hamster-selenium-core/src/test/java/com/github/grossopa/selenium/core/locator/SimpleXpathBuilderTest.java +++ b/hamster-selenium-core/src/test/java/com/github/grossopa/selenium/core/locator/SimpleXpathBuilderTest.java @@ -1,5 +1,5 @@ /* - * Copyright © 2021 the original author or authors. + * Copyright © 2023 the original author or authors. * * Licensed under the The MIT License (MIT) (the "License"); * You may obtain a copy of the License at @@ -253,8 +253,42 @@ void testBuilder32() { @Test void testBuilder33() { - assertEquals(By2.xpath("./div"), - builder.relative("div").build()); + assertEquals(By2.xpath("./div"), builder.relative("div").build()); + } + + @Test + void testBuilder34() { + assertEquals(By2.xpath("div[not(starts-with(@some-attr,'start\"char'))]" + + "/preceding::span/following-sibling::tag[not(matches(@data-value,\"some-pattern\"))]"), + builder.empty("div").attr("some-attr").not().startsWith("start\"char").preceding("span").axes() + .followingSibling("tag").attr("data-value").not().matches("some-pattern").build()); + } + + @Test + void testBuilder35() { + assertEquals(By2.xpath( + "div[not(starts-with(@some-attr,'start\"char')) and text()=\"some char\" or starts-with(name(),\"abc\")]" + + "/preceding::span/following-sibling::tag[not(matches(@data-value,\"some-pattern\"))]"), + builder.empty("div").attr("some-attr").not().startsWith("start\"char").and().text().exact("some char") + .or().name().startsWith("abc").preceding("span").axes().followingSibling("tag") + .attr("data-value").not().matches("some-pattern").build()); + } + + @Test + void testBuilder36() { + assertEquals(builder.anywhereRelative("div").attr("abc").not().contains("123").build(), + builder.anywhereRelative("div").attr("@abc").not().contains("123").build()); + } + + @Test + void testXpathString1() { + assertEquals("//*[contains(text(),\"abc\")]", builder.anywhere().text().contains("abc").xpathString()); + } + + @Test + void testXpathString2() { + assertEquals("//*[contains(text(),\"abc\")]/ancestor::*", + builder.anywhere().text().contains("abc").ancestor().xpathString()); } } diff --git a/hamster-selenium-examples/src/test/java/com/github/grossopa/selenium/examples/StartDriverServiceWinEdge.java b/hamster-selenium-examples/src/test/java/com/github/grossopa/selenium/examples/StartDriverServiceWinEdge.java index 8fe43cc6..1ac68c3b 100644 --- a/hamster-selenium-examples/src/test/java/com/github/grossopa/selenium/examples/StartDriverServiceWinEdge.java +++ b/hamster-selenium-examples/src/test/java/com/github/grossopa/selenium/examples/StartDriverServiceWinEdge.java @@ -39,7 +39,7 @@ */ public class StartDriverServiceWinEdge { - public static final String EXECUTABLE_PATH = "C:\\work\\software\\drivers\\msedgedriver_107.0.1418.35.exe"; + public static final String EXECUTABLE_PATH = "C:\\work\\software\\drivers\\msedgedriver_111.0.1661.51.exe"; public static final int PORT = 38383; @@ -47,7 +47,7 @@ public class StartDriverServiceWinEdge { public static void main(String[] args) throws IOException { DriverConfig config = new DriverConfig(); config.setDriverExecutablePath(EXECUTABLE_PATH); - config.setDriverVersion("107.0.1418.35"); + config.setDriverVersion("111.0.1661.51"); config.setType(WebDriverType.EDGE); config.setPort(PORT); diff --git a/hamster-selenium-examples/src/test/java/com/github/grossopa/selenium/examples/html/HtmlShowCase.java b/hamster-selenium-examples/src/test/java/com/github/grossopa/selenium/examples/html/HtmlShowCase.java index ab055478..a1a719ad 100644 --- a/hamster-selenium-examples/src/test/java/com/github/grossopa/selenium/examples/html/HtmlShowCase.java +++ b/hamster-selenium-examples/src/test/java/com/github/grossopa/selenium/examples/html/HtmlShowCase.java @@ -89,6 +89,25 @@ public void testTableNoHeader() { assertEquals("700", table.getBodyRow(2).getCells().get(0).getText()); assertEquals("800", table.getBodyRow(2).getCells().get(1).getText()); assertEquals("900", table.getBodyRow(2).getCells().get(2).getText()); + + List components = table.findComponents( + xpathBuilder().anywhereRelative("td").text().not().exact("200").build()); + assertEquals(8, components.size()); + assertEquals("100", components.get(0).getText()); + assertEquals("300", components.get(1).getText()); + assertEquals("400", components.get(2).getText()); + assertEquals("500", components.get(3).getText()); + assertEquals("600", components.get(4).getText()); + assertEquals("700", components.get(5).getText()); + assertEquals("800", components.get(6).getText()); + assertEquals("900", components.get(7).getText()); + + + components = table.findComponents( + xpathBuilder().anywhereRelative("td").text().exact("400").or().text().contains("6").build()); + assertEquals(2, components.size()); + assertEquals("400", components.get(0).getText()); + assertEquals("600", components.get(1).getText()); } public void testSelect() {