Skip to content

Commit

Permalink
#389 Support More Complex CSS Selectors
Browse files Browse the repository at this point in the history
Create predicates for css selectors in separate class.
Configure supported css selectors via regex expression and factory
method.
  • Loading branch information
briemla committed Jan 21, 2020
1 parent 1296bd4 commit c84c724
Show file tree
Hide file tree
Showing 8 changed files with 293 additions and 105 deletions.
122 changes: 17 additions & 105 deletions src/main/java/de/retest/web/selenium/TestHealer.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@
import static de.retest.web.selenium.ByWhisperer.retrievePartialLinkText;

import java.util.function.Consumer;
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;
Expand All @@ -33,17 +33,15 @@
import de.retest.recheck.ui.descriptors.IdentifyingAttributes;
import de.retest.recheck.ui.descriptors.RootElement;
import de.retest.recheck.ui.diff.ElementIdentificationWarning;
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";

Expand Down Expand Up @@ -98,7 +96,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 );
Expand All @@ -113,7 +111,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 );
Expand All @@ -128,7 +126,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 );
Expand All @@ -143,7 +141,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 );
Expand All @@ -158,7 +156,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 );
Expand All @@ -172,50 +170,14 @@ private WebElement findElementByPartialLinkText( final ByPartialLinkText by ) {

private WebElement findElementByCssSelector( final ByCssSelector by ) {
final String origSelector = ByWhisperer.retrieveCssSelector( by );
String selector = origSelector;
Predicate<Element> 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<Element>> 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<Predicate<Element>, WebElement> toWebElement = p -> toWebElement( origSelector, p );
return predicate.map( toWebElement ).orElse( null );
}

private WebElement toWebElement( final String origSelector, final Predicate<Element> predicate ) {
final Element actualElement =
de.retest.web.selenium.By.findElement( lastExpectedState, lastActualState, predicate );
if ( actualElement == null ) {
Expand All @@ -228,56 +190,6 @@ private WebElement findElementByCssSelector( final ByCssSelector by ) {
}
}

private static Predicate<Element> hasAttribute( final String attribute, final String attributeValue ) {
return element -> element.getAttributeValue( attribute ).toString().equals( attributeValue );
}

private static Predicate<Element> 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<Element> hasPartialLinkText( final String linkText ) {
return element -> "a".equalsIgnoreCase( element.getIdentifyingAttributes().getType() )
&& element.getAttributeValue( TEXT ).toString().contains( linkText );
}

private static Predicate<Element> hasClass( final String cssClass ) {
return element -> element.getIdentifyingAttributes().get( CLASS ) != null
? ((String) element.getIdentifyingAttributes().get( CLASS )).contains( cssClass ) : false;
}

private static Predicate<Element> hasName( final String name ) {
return element -> name.equals( element.getIdentifyingAttributes().get( NAME ) );
}

private static Predicate<Element> hasTag( final String tag ) {
return element -> element.getIdentifyingAttributes().get( TYPE ).equals( tag );
}

private static Predicate<Element> 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\"|'@\\*].*" );
}
Expand Down Expand Up @@ -315,7 +227,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 );
Expand Down
43 changes: 43 additions & 0 deletions src/main/java/de/retest/web/selenium/css/DefaultSelectors.java
Original file line number Diff line number Diff line change
@@ -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<String, Predicate<Element>> 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<Transformer> all() {
final LinkedList<Tupel> 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() );
}

}
30 changes: 30 additions & 0 deletions src/main/java/de/retest/web/selenium/css/EmptySelector.java
Original file line number Diff line number Diff line change
@@ -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<Element> predicate() {
return e -> false;
}

@Override
public String remainingSelector() {
return selector;
}

}
72 changes: 72 additions & 0 deletions src/main/java/de/retest/web/selenium/css/Has.java
Original file line number Diff line number Diff line change
@@ -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<Element> cssAttribute( final String withoutBrackets ) {
final String attribute = getAttribute( withoutBrackets );
final String attributeValue = getAttributeValue( withoutBrackets );
return hasAttribute( attribute, attributeValue );
}

private static Predicate<Element> 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<Element> 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<Element> partialLinkText( final String linkText ) {
return element -> "a".equalsIgnoreCase( element.getIdentifyingAttributes().getType() )
&& element.getAttributeValue( TEXT ).toString().contains( linkText );
}

public static Predicate<Element> cssClass( final String cssClass ) {
return element -> element.getIdentifyingAttributes().get( CLASS ) != null
? ((String) element.getIdentifyingAttributes().get( CLASS )).contains( cssClass ) : false;
}

public static Predicate<Element> cssName( final String name ) {
return element -> name.equals( element.getIdentifyingAttributes().get( NAME ) );
}

public static Predicate<Element> cssTag( final String tag ) {
return element -> element.getIdentifyingAttributes().get( TYPE ).equals( tag );
}

public static Predicate<Element> cssId( final String id ) {
return element -> id.equals( element.getIdentifyingAttributes().get( ID ) );
}
}
Loading

0 comments on commit c84c724

Please sign in to comment.