Skip to content

Commit

Permalink
#389 Support More Complex CSS Selectors
Browse files Browse the repository at this point in the history
Refactor Has class to simplify implementation of attribute selectors.
Implement all attribute selectors.
  • Loading branch information
briemla committed Jan 9, 2020
1 parent 3de491c commit 3a64bb7
Show file tree
Hide file tree
Showing 4 changed files with 273 additions and 61 deletions.
39 changes: 28 additions & 11 deletions src/main/java/de/retest/web/selenium/css/DefaultSelectors.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
public class DefaultSelectors {

@RequiredArgsConstructor
private static class Tupel {
private static class Rule {
private final String pattern;
private final Function<String, Predicate<Element>> factory;

Expand All @@ -24,20 +24,37 @@ private RegexTransformer createTransformer() {
}
}

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 CHARACTERSET = "a-zA-Z0-9\\-_";
private static final String ALLOWED_CHARACTERS = "[" + CHARACTERSET + "]+";
private static final String TAG_PATTERN = "(" + ALLOWED_CHARACTERS + ")";
private static final String ID_PATTERN = "\\#(" + ALLOWED_CHARACTERS + ")";
private static final String CLASS_PATTERN = "\\.(" + ALLOWED_CHARACTERS + ")";
private static final String ATTRIBUTE_PATTERN = "\\[([" + CHARACTERSET + "=\"]+)\\]";
private static final String ATTRIBUTE_CONTAINING_PATTERN = attributePattern( "~" );
private static final String ATTRIBUTE_STARTING_PATTERN = attributePattern( "\\|" );
private static final String ATTRIBUTE_BEGINNING_PATTERN = attributePattern( "\\^" );
private static final String ATTRIBUTE_ENDING_PATTERN = attributePattern( "\\$" );
private static final String ATTRIBUTE_CONTAINING_SUBSTRING_PATTERN = attributePattern( "\\*" );

private static String attributePattern( final String selectorChar ) {
return "\\[([" + CHARACTERSET + selectorChar + "=\"]+)\\]";
}

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() );
final LinkedList<Rule> tupels = new LinkedList<>();
tupels.add( new Rule( TAG_PATTERN, Has::cssTag ) );
tupels.add( new Rule( ID_PATTERN, Has::cssId ) );
tupels.add( new Rule( CLASS_PATTERN, Has::cssClass ) );
tupels.add( new Rule( ATTRIBUTE_PATTERN, Has::attribute ) );
tupels.add( new Rule( ATTRIBUTE_CONTAINING_PATTERN, Has::attributeContaining ) );
tupels.add( new Rule( ATTRIBUTE_STARTING_PATTERN, Has::attributeStarting ) );
tupels.add( new Rule( ATTRIBUTE_BEGINNING_PATTERN, Has::attributeBeginning ) );
tupels.add( new Rule( ATTRIBUTE_ENDING_PATTERN, Has::attributeEnding ) );
tupels.add( new Rule( ATTRIBUTE_CONTAINING_SUBSTRING_PATTERN, Has::attributeContainingSubstring ) );
return tupels.stream().map( Rule::createTransformer ).collect( toList() );
}

}
68 changes: 52 additions & 16 deletions src/main/java/de/retest/web/selenium/css/Has.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,43 +5,79 @@
import static de.retest.web.AttributesUtil.NAME;
import static de.retest.web.AttributesUtil.TEXT;

import java.util.function.BiPredicate;
import java.util.function.Predicate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

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;
private static final Pattern attribute = attributePattern( "" );
private static final Pattern attributeContaining = attributePattern( "~" );
private static final Pattern attributeStarting = attributePattern( "\\|" );
private static final Pattern attributeBeginning = attributePattern( "\\^" );
private static final Pattern attributeEnding = attributePattern( "\\$" );
private static final Pattern attributeContainsSubstring = attributePattern( "\\*" );

public static Predicate<Element> cssAttribute( final String withoutBrackets ) {
final String attribute = getAttribute( withoutBrackets );
final String attributeValue = getAttributeValue( withoutBrackets );
return hasAttribute( attribute, attributeValue );
private static Pattern attributePattern( final String selectingChar ) {
final String allowedCharacters = "[^" + selectingChar + "=]+";
return Pattern.compile( "(" + allowedCharacters + ")(" + selectingChar + "=(" + allowedCharacters + "))?" );
}

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

private static String getAttribute( final String withoutBrackets ) {
if ( withoutBrackets.contains( "=" ) ) {
return withoutBrackets.substring( 0, withoutBrackets.lastIndexOf( "=" ) );
private static Predicate<Element> hasAttribute( final String selector, final Pattern pattern,
final BiPredicate<String, String> predicate ) {
final Matcher matcher = pattern.matcher( selector );
if ( matcher.matches() ) {
final String attribute = matcher.group( 1 );
final String attributeValue = clearQuotes( matcher.group( 3 ) );
return hasAttributeValue( attribute, attributeValue, predicate );
}
return withoutBrackets;
return e -> false;
}

private static String getAttributeValue( final String withoutBrackets ) {
if ( !withoutBrackets.contains( "=" ) ) {
private static Predicate<Element> hasAttributeValue( final String attribute, final String attributeValue,
final BiPredicate<String, String> toPredicate ) {
return element -> toPredicate.test( element.getAttributeValue( attribute ).toString(), attributeValue );
}

private static String clearQuotes( final String result ) {
if ( null == result ) {
return "true";
}
String result = withoutBrackets.substring( withoutBrackets.lastIndexOf( "=" ) + 1 );
if ( result.contains( "\"" ) || result.contains( "'" ) ) {
result = result.substring( 1, result.length() - 1 );
return result.substring( 1, result.length() - 1 );
}
return result;
}

public static Predicate<Element> attributeContaining( final String selector ) {
return hasAttribute( selector, attributeContaining, String::contains );
}

public static Predicate<Element> attributeStarting( final String selector ) {
return hasAttribute( selector, attributeStarting, String::startsWith );
}

public static Predicate<Element> attributeBeginning( final String selector ) {
return hasAttribute( selector, attributeBeginning, String::startsWith );
}

public static Predicate<Element> attributeEnding( final String selector ) {
return hasAttribute( selector, attributeEnding, String::endsWith );
}

public static Predicate<Element> attributeContainingSubstring( final String selector ) {
return hasAttribute( selector, attributeContainsSubstring, String::contains );
}

public static Predicate<Element> linkText( final String linkText ) {
return element -> "a".equalsIgnoreCase( element.getIdentifyingAttributes().getType() )
&& linkText.equals( element.getAttributes().get( TEXT ) )
Expand All @@ -55,15 +91,15 @@ public static Predicate<Element> partialLinkText( final String linkText ) {

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

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 );
return element -> tag.equals( element.getIdentifyingAttributes().get( TYPE ) );
}

public static Predicate<Element> cssId( final String id ) {
Expand Down
80 changes: 46 additions & 34 deletions src/test/java/de/retest/web/selenium/TestHealerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,19 @@
import static de.retest.web.selenium.TestHealer.findElement;
import static de.retest.web.selenium.TestHealer.isNotYetSupportedXPathExpression;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.stream.Stream;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.function.Executable;
import org.openqa.selenium.By;
import org.slf4j.LoggerFactory;

Expand Down Expand Up @@ -87,6 +90,41 @@ public void ByCssSelector_using_tag_with_hyphen_should_redirect() {

@Test
public void ByCssSelector_matches_elements_with_given_attribute() {
configureByCssSelectorAttributeTests();

assertAll( Stream.of( //
"[data-id=\"myspecialID\"]", //
"[disabled]", //
"div[data-id=\"myspecialID\"]", //
"div[disabled]", //
".myClass[data-id=\"myspecialID\"]", //
".myClass[disabled]", //
"#myId[data-id=\"myspecialID\"]", //
"#myId[disabled]" ) //
.map( this::assertByCssSelector ) );
}

@Test
public void ByCssSelector_matches_elements_with_given_attribute_value() {
configureByCssSelectorAttributeTests();

assertAll( assertAttributeValues( "~", "=\"special\"]" ) );
assertAll( assertAttributeValues( "|", "=\"myspecial\"]" ) );
assertAll( assertAttributeValues( "^", "=\"myspecial\"]" ) );
assertAll( assertAttributeValues( "$", "=\"specialID\"]" ) );
assertAll( assertAttributeValues( "*", "=\"special\"]" ) );
}

private Stream<Executable> assertAttributeValues( final String selectorChar, final String value ) {
return Stream.of( //
"[data-id" + selectorChar + value, //
"div[data-id" + selectorChar + value, //
".myClass[data-id" + selectorChar + value, //
"#myId[data-id" + selectorChar + value ) //
.map( this::assertByCssSelector );
}

private void configureByCssSelectorAttributeTests() {
final MutableAttributes attributes = new MutableAttributes();
attributes.put( "data-id", "myspecialID" );
attributes.put( "disabled", "true" );
Expand All @@ -98,18 +136,6 @@ public void ByCssSelector_matches_elements_with_given_attribute() {
final Element element = create( ID, state, new IdentifyingAttributes( identCrit ), attributes.immutable() );
when( state.getContainedElements() ).thenReturn( Collections.singletonList( element ) );
when( wrapped.findElement( By.xpath( xpath ) ) ).thenReturn( resultMarker );

assertThat( findElement( By.cssSelector( "[data-id=\"myspecialID\"]" ), wrapped ) ).isEqualTo( resultMarker );
assertThat( findElement( By.cssSelector( "[disabled]" ), wrapped ) ).isEqualTo( resultMarker );
assertThat( findElement( By.cssSelector( "div[data-id=\"myspecialID\"]" ), wrapped ) )
.isEqualTo( resultMarker );
assertThat( findElement( By.cssSelector( "div[disabled]" ), wrapped ) ).isEqualTo( resultMarker );
assertThat( findElement( By.cssSelector( ".myClass[data-id=\"myspecialID\"]" ), wrapped ) )
.isEqualTo( resultMarker );
assertThat( findElement( By.cssSelector( ".myClass[disabled]" ), wrapped ) ).isEqualTo( resultMarker );
assertThat( findElement( By.cssSelector( "#myId[data-id=\"myspecialID\"]" ), wrapped ) )
.isEqualTo( resultMarker );
assertThat( findElement( By.cssSelector( "#myId[disabled]" ), wrapped ) ).isEqualTo( resultMarker );
}

@Test
Expand All @@ -122,15 +148,20 @@ public void ByCssSelector_matches_elements_with_given_class() {
when( state.getContainedElements() ).thenReturn( Collections.singletonList( element ) );
when( wrapped.findElement( By.xpath( xpath ) ) ).thenReturn( resultMarker );

assertThat( findElement( By.cssSelector( ".pure-button" ), wrapped ) ).isEqualTo( resultMarker );
assertThat( findElement( By.cssSelector( ".pure-button.my-button" ), wrapped ) ).isEqualTo( resultMarker );
assertThat( findElement( By.cssSelector( ".pure-button .my-button" ), wrapped ) ).isEqualTo( resultMarker );
assertAll( assertByCssSelector( ".pure-button" ), //
assertByCssSelector( ".pure-button.my-button" ), //
assertByCssSelector( ".pure-button .my-button" ) );

assertThat( findElement( By.cssSelector( ".special-class" ), wrapped ) ).isEqualTo( null );
assertThat( findElement( By.cssSelector( ".pure-button.special-class" ), wrapped ) ).isEqualTo( null );
assertThat( findElement( By.cssSelector( ".pure-button .special-class" ), wrapped ) ).isEqualTo( null );
}

private Executable assertByCssSelector( final String hierarchicalClass ) {
return () -> assertThat( findElement( By.cssSelector( hierarchicalClass ), wrapped ) )
.isEqualTo( resultMarker );
}

@Test
public void empty_selectors_should_not_throw_exception() {
assertThat( findElement( By.cssSelector( "" ), wrapped ) ).isNull();
Expand Down Expand Up @@ -162,36 +193,17 @@ public void not_yet_implemented_ByCssSelector_should_be_logged() {
assertThat( logsList.get( 0 ).getArgumentArray()[0] ).isEqualTo( ":not(:first-child):not(:last-child)" );
logsList.clear();

assertThat( findElement( By.cssSelector( ".input-group[class*=\"col-\"]" ), wrapped ) ).isNull();
assertThat( logsList.get( 0 ).getMessage() )
.startsWith( "Unbreakable tests are not implemented for all CSS selectors." );
assertThat( logsList.get( 0 ).getArgumentArray()[0] ).isEqualTo( "[class*=\"col-\"]" );
logsList.clear();

assertThat( findElement( By.cssSelector( "div~p" ), wrapped ) ).isNull();
assertThat( logsList.get( 0 ).getMessage() )
.startsWith( "Unbreakable tests are not implemented for all CSS selectors." );
assertThat( logsList.get( 0 ).getArgumentArray()[0] ).isEqualTo( "~p" );
logsList.clear();

assertThat( findElement( By.cssSelector( "[href*=\"w3schools\"]" ), wrapped ) ).isNull();
assertThat( logsList.get( 0 ).getMessage() )
.startsWith( "Unbreakable tests are not implemented for all CSS selectors." );
assertThat( logsList.get( 0 ).getArgumentArray()[0] ).isEqualTo( "[href*=\"w3schools\"]" );
logsList.clear();

assertThat( findElement( By.cssSelector( "div,p" ), wrapped ) ).isNull();
assertThat( logsList.get( 0 ).getMessage() )
.startsWith( "Unbreakable tests are not implemented for all CSS selectors." );
assertThat( logsList.get( 0 ).getArgumentArray()[0] ).isEqualTo( ",p" );
logsList.clear();

// TODO
// [attribute~=value] [title~=flower] Selects all elements with a title attribute containing the word "flower"
// [attribute|=value] [lang|=en] Selects all elements with a lang attribute value starting with "en"
// [attribute^=value] a[href^="https"] Selects every element whose href attribute value begins with "https"
// [attribute$=value] a[href$=".pdf"] Selects every element whose href attribute value ends with ".pdf"
// [attribute*=value] a[href*="w3schools"] Selects every element whose href attribute value contains the substring "w3schools"
}

@Test
Expand Down
Loading

0 comments on commit 3a64bb7

Please sign in to comment.