From b7e7b505d5b7963b806d08f2c6e1a8c1ddb8a53d Mon Sep 17 00:00:00 2001 From: Lars Briem Date: Sun, 24 Nov 2019 16:54:26 +0100 Subject: [PATCH] #389 Support More Complex CSS Selectors Create predicates for css selectors in separate class. Configure supported css selectors via regex expression and factory method. --- .../de/retest/web/selenium/TestHealer.java | 122 +++--------------- .../web/selenium/css/DefaultSelectors.java | 43 ++++++ .../web/selenium/css/EmptySelector.java | 30 +++++ .../java/de/retest/web/selenium/css/Has.java | 72 +++++++++++ .../web/selenium/css/PredicateBuilder.java | 53 ++++++++ .../web/selenium/css/RegexSelector.java | 35 +++++ .../de/retest/web/selenium/css/Selector.java | 15 +++ .../retest/web/selenium/css/Transformer.java | 28 ++++ 8 files changed, 293 insertions(+), 105 deletions(-) create mode 100644 src/main/java/de/retest/web/selenium/css/DefaultSelectors.java create mode 100644 src/main/java/de/retest/web/selenium/css/EmptySelector.java create mode 100644 src/main/java/de/retest/web/selenium/css/Has.java create mode 100644 src/main/java/de/retest/web/selenium/css/PredicateBuilder.java create mode 100644 src/main/java/de/retest/web/selenium/css/RegexSelector.java create mode 100644 src/main/java/de/retest/web/selenium/css/Selector.java create mode 100644 src/main/java/de/retest/web/selenium/css/Transformer.java diff --git a/src/main/java/de/retest/web/selenium/TestHealer.java b/src/main/java/de/retest/web/selenium/TestHealer.java index 576d910bf..26768bed8 100644 --- a/src/main/java/de/retest/web/selenium/TestHealer.java +++ b/src/main/java/de/retest/web/selenium/TestHealer.java @@ -10,9 +10,9 @@ import static de.retest.web.selenium.ByWhisperer.retrieveName; import static de.retest.web.selenium.ByWhisperer.retrievePartialLinkText; +import java.util.Optional; +import java.util.function.Function; import java.util.function.Predicate; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import org.openqa.selenium.By; import org.openqa.selenium.By.ByClassName; @@ -31,17 +31,15 @@ import de.retest.recheck.ui.descriptors.Element; import de.retest.recheck.ui.descriptors.IdentifyingAttributes; import de.retest.recheck.ui.descriptors.RootElement; +import de.retest.web.selenium.css.DefaultSelectors; +import de.retest.web.selenium.css.Has; +import de.retest.web.selenium.css.PredicateBuilder; public class TestHealer { private static final String PATH = IdentifyingAttributes.PATH_ATTRIBUTE_KEY; private static final String TYPE = IdentifyingAttributes.TYPE_ATTRIBUTE_KEY; - private static final Pattern CSS_CLASS = Pattern.compile( "^\\.([a-zA-Z0-9\\-]+)" ); - private static final Pattern CSS_ID = Pattern.compile( "^\\#([a-zA-Z0-9\\-]+)" ); - private static final Pattern CSS_TAG = Pattern.compile( "^([a-zA-Z0-9\\-]+)" ); - private static final Pattern CSS_ATTRIBUTE = Pattern.compile( "^\\[([a-zA-Z0-9\\-=\"]+)\\]" ); - private static final Logger logger = LoggerFactory.getLogger( TestHealer.class ); private static final String ELEMENT_NOT_FOUND_MESSAGE = "It appears that even the Golden Master has no element"; @@ -94,7 +92,7 @@ private WebElement findElement( final By by ) { private WebElement findElementById( final ById by ) { final String id = retrieveId( by ); final Element actualElement = - de.retest.web.selenium.By.findElement( lastExpectedState, lastActualState, hasID( id ) ); + de.retest.web.selenium.By.findElement( lastExpectedState, lastActualState, Has.cssId( id ) ); if ( actualElement == null ) { logger.warn( "{} with id '{}'.", ELEMENT_NOT_FOUND_MESSAGE, id ); @@ -109,7 +107,7 @@ private WebElement findElementById( final ById by ) { private WebElement findElementByClassName( final ByClassName by ) { final String className = retrieveCssClassName( by ); final Element actualElement = - de.retest.web.selenium.By.findElement( lastExpectedState, lastActualState, hasClass( className ) ); + de.retest.web.selenium.By.findElement( lastExpectedState, lastActualState, Has.cssClass( className ) ); if ( actualElement == null ) { logger.warn( "{} with CSS class '{}'.", ELEMENT_NOT_FOUND_MESSAGE, className ); @@ -124,7 +122,7 @@ private WebElement findElementByClassName( final ByClassName by ) { private WebElement findElementByName( final ByName by ) { final String name = retrieveName( by ); final Element actualElement = - de.retest.web.selenium.By.findElement( lastExpectedState, lastActualState, hasName( name ) ); + de.retest.web.selenium.By.findElement( lastExpectedState, lastActualState, Has.cssName( name ) ); if ( actualElement == null ) { logger.warn( "{} with name '{}'.", ELEMENT_NOT_FOUND_MESSAGE, name ); @@ -139,7 +137,7 @@ private WebElement findElementByName( final ByName by ) { private WebElement findElementByLinkText( final ByLinkText by ) { final String linkText = retrieveLinkText( by ); final Element actualElement = - de.retest.web.selenium.By.findElement( lastExpectedState, lastActualState, hasLinkText( linkText ) ); + de.retest.web.selenium.By.findElement( lastExpectedState, lastActualState, Has.linkText( linkText ) ); if ( actualElement == null ) { logger.warn( "{} with link text '{}'.", ELEMENT_NOT_FOUND_MESSAGE, linkText ); @@ -154,7 +152,7 @@ private WebElement findElementByLinkText( final ByLinkText by ) { private WebElement findElementByPartialLinkText( final ByPartialLinkText by ) { final String partialLinkText = retrievePartialLinkText( by ); final Element actualElement = de.retest.web.selenium.By.findElement( lastExpectedState, lastActualState, - hasPartialLinkText( partialLinkText ) ); + Has.partialLinkText( partialLinkText ) ); if ( actualElement == null ) { logger.warn( "{} with link text '{}'.", ELEMENT_NOT_FOUND_MESSAGE, partialLinkText ); @@ -169,50 +167,14 @@ private WebElement findElementByPartialLinkText( final ByPartialLinkText by ) { private WebElement findElementByCssSelector( final ByCssSelector by ) { final String origSelector = ByWhisperer.retrieveCssSelector( by ); - String selector = origSelector; - Predicate predicate = element -> true; - boolean matched = true; - while ( !selector.isEmpty() && matched ) { - matched = false; - final Matcher tagMatcher = CSS_TAG.matcher( selector ); - if ( tagMatcher.find() ) { - final String tag = tagMatcher.group( 1 ); - predicate = predicate.and( hasTag( tag ) ); - selector = selector.substring( tag.length() ).trim(); - matched = true; - } - final Matcher idMatcher = CSS_ID.matcher( selector ); - if ( idMatcher.find() ) { - final String id = idMatcher.group( 1 ); - predicate = predicate.and( hasID( id ) ); - selector = selector.substring( id.length() + 1 ).trim(); - matched = true; - } - final Matcher classMatcher = CSS_CLASS.matcher( selector ); - if ( classMatcher.find() ) { - final String cssClass = classMatcher.group( 1 ); - predicate = predicate.and( hasClass( cssClass ) ); - selector = selector.substring( cssClass.length() + 1 ).trim(); - matched = true; - } - final Matcher attributeMatcher = CSS_ATTRIBUTE.matcher( selector ); - if ( attributeMatcher.find() ) { - final String withoutBrackets = attributeMatcher.group( 1 ); - final String attribute = getAttribute( withoutBrackets ); - final String attributeValue = getAttributeValue( withoutBrackets ); - predicate = predicate.and( hasAttribute( attribute, attributeValue ) ); - selector = selector.substring( withoutBrackets.length() + 2 ).trim(); - matched = true; - } - } + final Optional> predicate = + new PredicateBuilder( DefaultSelectors.all(), origSelector ).build(); - if ( !selector.isEmpty() ) { - logger.warn( - "Unbreakable tests are not implemented for all CSS selectors. Please report your chosen selector ('{}') at https://github.com/retest/recheck-web/issues.", - selector ); - return null; - } + final Function, WebElement> toWebElement = p -> toWebElement( origSelector, p ); + return predicate.map( toWebElement ).orElse( null ); + } + private WebElement toWebElement( final String origSelector, final Predicate predicate ) { final Element actualElement = de.retest.web.selenium.By.findElement( lastExpectedState, lastActualState, predicate ); if ( actualElement == null ) { @@ -225,56 +187,6 @@ private WebElement findElementByCssSelector( final ByCssSelector by ) { } } - private static Predicate hasAttribute( final String attribute, final String attributeValue ) { - return element -> element.getAttributeValue( attribute ).toString().equals( attributeValue ); - } - - private static Predicate hasLinkText( final String linkText ) { - return element -> "a".equalsIgnoreCase( element.getIdentifyingAttributes().getType() ) - && linkText.equals( element.getAttributes().get( TEXT ) ) - || linkText.equals( element.getIdentifyingAttributes().get( TEXT ) ); - } - - private static Predicate hasPartialLinkText( final String linkText ) { - return element -> "a".equalsIgnoreCase( element.getIdentifyingAttributes().getType() ) - && element.getAttributeValue( TEXT ).toString().contains( linkText ); - } - - private static Predicate hasClass( final String cssClass ) { - return element -> element.getIdentifyingAttributes().get( CLASS ) != null - ? ((String) element.getIdentifyingAttributes().get( CLASS )).contains( cssClass ) : false; - } - - private static Predicate hasName( final String name ) { - return element -> name.equals( element.getIdentifyingAttributes().get( NAME ) ); - } - - private static Predicate hasTag( final String tag ) { - return element -> element.getIdentifyingAttributes().get( TYPE ).equals( tag ); - } - - private static Predicate hasID( final String id ) { - return element -> id.equals( element.getIdentifyingAttributes().get( ID ) ); - } - - private String getAttribute( final String withoutBrackets ) { - if ( withoutBrackets.contains( "=" ) ) { - return withoutBrackets.substring( 0, withoutBrackets.lastIndexOf( "=" ) ); - } - return withoutBrackets; - } - - private String getAttributeValue( final String withoutBrackets ) { - if ( !withoutBrackets.contains( "=" ) ) { - return "true"; - } - String result = withoutBrackets.substring( withoutBrackets.lastIndexOf( "=" ) + 1 ); - if ( result.contains( "\"" ) || result.contains( "'" ) ) { - result = result.substring( 1, result.length() - 1 ); - } - return result; - } - protected static boolean isNotYetSupportedXPathExpression( final String xpathExpression ) { return xpathExpression.matches( ".*[<>:+\\s\"|'@\\*].*" ); } @@ -312,7 +224,7 @@ private WebElement findElementByXPath( final ByXPath byXPath ) { private WebElement findElementByTagName( final ByTagName by ) { final String tag = ByWhisperer.retrieveTag( by ); final Element actualElement = - de.retest.web.selenium.By.findElement( lastExpectedState, lastActualState, hasTag( tag ) ); + de.retest.web.selenium.By.findElement( lastExpectedState, lastActualState, Has.cssTag( tag ) ); if ( actualElement == null ) { logger.warn( "{} with tag '{}'.", ELEMENT_NOT_FOUND_MESSAGE, tag ); diff --git a/src/main/java/de/retest/web/selenium/css/DefaultSelectors.java b/src/main/java/de/retest/web/selenium/css/DefaultSelectors.java new file mode 100644 index 000000000..ec28b9400 --- /dev/null +++ b/src/main/java/de/retest/web/selenium/css/DefaultSelectors.java @@ -0,0 +1,43 @@ +package de.retest.web.selenium.css; + +import static java.util.stream.Collectors.toList; + +import java.util.LinkedList; +import java.util.List; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.regex.Pattern; + +import de.retest.recheck.ui.descriptors.Element; +import lombok.RequiredArgsConstructor; + +public class DefaultSelectors { + + @RequiredArgsConstructor + private static class Tupel { + private final String pattern; + private final Function> factory; + + private Transformer createTransformer() { + final Pattern cssTag = Pattern.compile( START_OF_LINE + pattern + REMAINING ); + return new Transformer( cssTag, factory ); + } + } + + private static final String TAG_PATTERN = "([a-zA-Z0-9\\-]+)"; + private static final String ID_PATTERN = "\\#([a-zA-Z0-9\\-]+)"; + private static final String CLASS_PATTERN = "\\.([a-zA-Z0-9\\-]+)"; + private static final String ATTRIBUTE_PATTERN = "\\[([a-zA-Z0-9\\-=\"]+)\\]"; + private static final String REMAINING = "(.*)$"; + private static final String START_OF_LINE = "^"; + + public static List all() { + final LinkedList tupels = new LinkedList<>(); + tupels.add( new Tupel( TAG_PATTERN, Has::cssTag ) ); + tupels.add( new Tupel( ID_PATTERN, Has::cssId ) ); + tupels.add( new Tupel( CLASS_PATTERN, Has::cssClass ) ); + tupels.add( new Tupel( ATTRIBUTE_PATTERN, Has::cssAttribute ) ); + return tupels.stream().map( Tupel::createTransformer ).collect( toList() ); + } + +} diff --git a/src/main/java/de/retest/web/selenium/css/EmptySelector.java b/src/main/java/de/retest/web/selenium/css/EmptySelector.java new file mode 100644 index 000000000..4c278136f --- /dev/null +++ b/src/main/java/de/retest/web/selenium/css/EmptySelector.java @@ -0,0 +1,30 @@ +package de.retest.web.selenium.css; + +import java.util.function.Predicate; + +import de.retest.recheck.ui.descriptors.Element; + +public class EmptySelector implements Selector { + + private final String selector; + + public EmptySelector( final String selector ) { + this.selector = selector; + } + + @Override + public boolean matches() { + return false; + } + + @Override + public Predicate predicate() { + return e -> false; + } + + @Override + public String remainingSelector() { + return selector; + } + +} diff --git a/src/main/java/de/retest/web/selenium/css/Has.java b/src/main/java/de/retest/web/selenium/css/Has.java new file mode 100644 index 000000000..880bba461 --- /dev/null +++ b/src/main/java/de/retest/web/selenium/css/Has.java @@ -0,0 +1,72 @@ +package de.retest.web.selenium.css; + +import static de.retest.web.AttributesUtil.CLASS; +import static de.retest.web.AttributesUtil.ID; +import static de.retest.web.AttributesUtil.NAME; +import static de.retest.web.AttributesUtil.TEXT; + +import java.util.function.Predicate; + +import de.retest.recheck.ui.descriptors.Element; +import de.retest.recheck.ui.descriptors.IdentifyingAttributes; + +public class Has { + + private static final String TYPE = IdentifyingAttributes.TYPE_ATTRIBUTE_KEY; + + public static Predicate cssAttribute( final String withoutBrackets ) { + final String attribute = getAttribute( withoutBrackets ); + final String attributeValue = getAttributeValue( withoutBrackets ); + return hasAttribute( attribute, attributeValue ); + } + + private static Predicate hasAttribute( final String attribute, final String attributeValue ) { + return element -> element.getAttributeValue( attribute ).toString().equals( attributeValue ); + } + + private static String getAttribute( final String withoutBrackets ) { + if ( withoutBrackets.contains( "=" ) ) { + return withoutBrackets.substring( 0, withoutBrackets.lastIndexOf( "=" ) ); + } + return withoutBrackets; + } + + private static String getAttributeValue( final String withoutBrackets ) { + if ( !withoutBrackets.contains( "=" ) ) { + return "true"; + } + String result = withoutBrackets.substring( withoutBrackets.lastIndexOf( "=" ) + 1 ); + if ( result.contains( "\"" ) || result.contains( "'" ) ) { + result = result.substring( 1, result.length() - 1 ); + } + return result; + } + + public static Predicate linkText( final String linkText ) { + return element -> "a".equalsIgnoreCase( element.getIdentifyingAttributes().getType() ) + && linkText.equals( element.getAttributes().get( TEXT ) ) + || linkText.equals( element.getIdentifyingAttributes().get( TEXT ) ); + } + + public static Predicate partialLinkText( final String linkText ) { + return element -> "a".equalsIgnoreCase( element.getIdentifyingAttributes().getType() ) + && element.getAttributeValue( TEXT ).toString().contains( linkText ); + } + + public static Predicate cssClass( final String cssClass ) { + return element -> element.getIdentifyingAttributes().get( CLASS ) != null + ? ((String) element.getIdentifyingAttributes().get( CLASS )).contains( cssClass ) : false; + } + + public static Predicate cssName( final String name ) { + return element -> name.equals( element.getIdentifyingAttributes().get( NAME ) ); + } + + public static Predicate cssTag( final String tag ) { + return element -> element.getIdentifyingAttributes().get( TYPE ).equals( tag ); + } + + public static Predicate cssId( final String id ) { + return element -> id.equals( element.getIdentifyingAttributes().get( ID ) ); + } +} diff --git a/src/main/java/de/retest/web/selenium/css/PredicateBuilder.java b/src/main/java/de/retest/web/selenium/css/PredicateBuilder.java new file mode 100644 index 000000000..ea53d97c3 --- /dev/null +++ b/src/main/java/de/retest/web/selenium/css/PredicateBuilder.java @@ -0,0 +1,53 @@ +package de.retest.web.selenium.css; + +import java.util.LinkedList; +import java.util.List; +import java.util.Optional; +import java.util.function.Predicate; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import de.retest.recheck.ui.descriptors.Element; +import de.retest.web.selenium.TestHealer; + +public class PredicateBuilder { + + private static final Logger logger = LoggerFactory.getLogger( TestHealer.class ); + + private final List selectors; + private final List> predicates; + private final String origSelector; + + public PredicateBuilder( final List selectors, final String origSelector ) { + this.selectors = selectors; + this.origSelector = origSelector; + predicates = new LinkedList<>(); + } + + public Optional> build() { + String selector = origSelector; + boolean matched = true; + while ( !selector.isEmpty() && matched ) { + matched = false; + for ( final Transformer function : selectors ) { + final Selector cssSelector = function.transform( selector ); + if ( cssSelector.matches() ) { + predicates.add( cssSelector.predicate() ); + selector = cssSelector.remainingSelector(); + matched = cssSelector.matches(); + } + } + } + + if ( !selector.isEmpty() ) { + logger.warn( + "Unbreakable tests are not implemented for all CSS selectors. Please report your chosen selector ('{}') at https://github.com/retest/recheck-web/issues.", + selector ); + return Optional.empty(); + + } + return Optional.of( predicates.stream().reduce( Predicate::and ).orElse( e -> true ) ); + } + +} diff --git a/src/main/java/de/retest/web/selenium/css/RegexSelector.java b/src/main/java/de/retest/web/selenium/css/RegexSelector.java new file mode 100644 index 000000000..ac069d6fd --- /dev/null +++ b/src/main/java/de/retest/web/selenium/css/RegexSelector.java @@ -0,0 +1,35 @@ +package de.retest.web.selenium.css; + +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.regex.Matcher; + +import de.retest.recheck.ui.descriptors.Element; + +public class RegexSelector implements Selector { + + private final Matcher classMatcher; + private final Function> factory; + + public RegexSelector( final Matcher classMatcher, final Function> factory ) { + this.classMatcher = classMatcher; + this.factory = factory; + } + + @Override + public Predicate predicate() { + final String cssAttribute = classMatcher.group( 1 ); + return factory.apply( cssAttribute ); + } + + @Override + public String remainingSelector() { + return classMatcher.group( 2 ).trim(); + } + + @Override + public boolean matches() { + return true; + } + +} diff --git a/src/main/java/de/retest/web/selenium/css/Selector.java b/src/main/java/de/retest/web/selenium/css/Selector.java new file mode 100644 index 000000000..cd19a9438 --- /dev/null +++ b/src/main/java/de/retest/web/selenium/css/Selector.java @@ -0,0 +1,15 @@ +package de.retest.web.selenium.css; + +import java.util.function.Predicate; + +import de.retest.recheck.ui.descriptors.Element; + +public interface Selector { + + boolean matches(); + + Predicate predicate(); + + String remainingSelector(); + +} diff --git a/src/main/java/de/retest/web/selenium/css/Transformer.java b/src/main/java/de/retest/web/selenium/css/Transformer.java new file mode 100644 index 000000000..e0e4d9d88 --- /dev/null +++ b/src/main/java/de/retest/web/selenium/css/Transformer.java @@ -0,0 +1,28 @@ +package de.retest.web.selenium.css; + +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import de.retest.recheck.ui.descriptors.Element; + +public class Transformer { + + private final Pattern cssPattern; + private final Function> factory; + + public Transformer( final Pattern cssPattern, final Function> factory ) { + this.cssPattern = cssPattern; + this.factory = factory; + } + + public Selector transform( final String selector ) { + final Matcher attributeMatcher = cssPattern.matcher( selector ); + if ( attributeMatcher.find() ) { + return new RegexSelector( attributeMatcher, factory ); + } + return new EmptySelector( selector ); + } + +}