From 3a64bb726283d2d21c3bbd9e66f01081411e11e3 Mon Sep 17 00:00:00 2001 From: Lars Briem Date: Sun, 1 Dec 2019 14:06:38 +0100 Subject: [PATCH] #389 Support More Complex CSS Selectors Refactor Has class to simplify implementation of attribute selectors. Implement all attribute selectors. --- .../web/selenium/css/DefaultSelectors.java | 39 +++-- .../java/de/retest/web/selenium/css/Has.java | 68 ++++++-- .../retest/web/selenium/TestHealerTest.java | 80 ++++++---- .../de/retest/web/selenium/css/HasTest.java | 147 ++++++++++++++++++ 4 files changed, 273 insertions(+), 61 deletions(-) create mode 100644 src/test/java/de/retest/web/selenium/css/HasTest.java diff --git a/src/main/java/de/retest/web/selenium/css/DefaultSelectors.java b/src/main/java/de/retest/web/selenium/css/DefaultSelectors.java index 639e72e2f..58fc6e1d7 100644 --- a/src/main/java/de/retest/web/selenium/css/DefaultSelectors.java +++ b/src/main/java/de/retest/web/selenium/css/DefaultSelectors.java @@ -14,7 +14,7 @@ public class DefaultSelectors { @RequiredArgsConstructor - private static class Tupel { + private static class Rule { private final String pattern; private final Function> factory; @@ -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 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() ); + final LinkedList 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() ); } } diff --git a/src/main/java/de/retest/web/selenium/css/Has.java b/src/main/java/de/retest/web/selenium/css/Has.java index 880bba461..d72778ad6 100644 --- a/src/main/java/de/retest/web/selenium/css/Has.java +++ b/src/main/java/de/retest/web/selenium/css/Has.java @@ -5,7 +5,10 @@ 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; @@ -13,35 +16,68 @@ 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 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 hasAttribute( final String attribute, final String attributeValue ) { - return element -> element.getAttributeValue( attribute ).toString().equals( attributeValue ); + public static Predicate 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 hasAttribute( final String selector, final Pattern pattern, + final BiPredicate 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 hasAttributeValue( final String attribute, final String attributeValue, + final BiPredicate 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 attributeContaining( final String selector ) { + return hasAttribute( selector, attributeContaining, String::contains ); + } + + public static Predicate attributeStarting( final String selector ) { + return hasAttribute( selector, attributeStarting, String::startsWith ); + } + + public static Predicate attributeBeginning( final String selector ) { + return hasAttribute( selector, attributeBeginning, String::startsWith ); + } + + public static Predicate attributeEnding( final String selector ) { + return hasAttribute( selector, attributeEnding, String::endsWith ); + } + + public static Predicate attributeContainingSubstring( final String selector ) { + return hasAttribute( selector, attributeContainsSubstring, String::contains ); + } + public static Predicate linkText( final String linkText ) { return element -> "a".equalsIgnoreCase( element.getIdentifyingAttributes().getType() ) && linkText.equals( element.getAttributes().get( TEXT ) ) @@ -55,7 +91,7 @@ public static Predicate partialLinkText( final String linkText ) { public static Predicate 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 cssName( final String name ) { @@ -63,7 +99,7 @@ public static Predicate cssName( final String name ) { } public static Predicate cssTag( final String tag ) { - return element -> element.getIdentifyingAttributes().get( TYPE ).equals( tag ); + return element -> tag.equals( element.getIdentifyingAttributes().get( TYPE ) ); } public static Predicate cssId( final String id ) { diff --git a/src/test/java/de/retest/web/selenium/TestHealerTest.java b/src/test/java/de/retest/web/selenium/TestHealerTest.java index e479b2b25..381794502 100644 --- a/src/test/java/de/retest/web/selenium/TestHealerTest.java +++ b/src/test/java/de/retest/web/selenium/TestHealerTest.java @@ -8,6 +8,7 @@ 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; @@ -15,9 +16,11 @@ 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; @@ -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 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" ); @@ -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 @@ -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(); @@ -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 diff --git a/src/test/java/de/retest/web/selenium/css/HasTest.java b/src/test/java/de/retest/web/selenium/css/HasTest.java new file mode 100644 index 000000000..5d666522b --- /dev/null +++ b/src/test/java/de/retest/web/selenium/css/HasTest.java @@ -0,0 +1,147 @@ +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 static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.mockito.Mockito.when; + +import java.util.function.Function; +import java.util.function.Predicate; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.function.Executable; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import de.retest.recheck.ui.descriptors.Attributes; +import de.retest.recheck.ui.descriptors.Element; +import de.retest.recheck.ui.descriptors.IdentifyingAttributes; + +@ExtendWith( MockitoExtension.class ) +class HasTest { + + private static final String TYPE = IdentifyingAttributes.TYPE_ATTRIBUTE_KEY; + + @Mock + private Element element; + @Mock + private IdentifyingAttributes identifyingAttribues; + @Mock + private Attributes attributes; + + @Test + void cssAttributes() throws Exception { + assertAll( createAssert( TYPE, Has::cssTag ), createAssert( NAME, Has::cssName ), + createAssert( CLASS, Has::cssClass ), createAssert( ID, Has::cssId ) ); + } + + private Executable createAssert( final String attribute, final Function> predicate ) { + final String value = "value"; + when( identifyingAttribues.get( attribute ) ).thenReturn( value ); + when( element.getIdentifyingAttributes() ).thenReturn( identifyingAttribues ); + return () -> assertThat( predicate.apply( value ).test( element ) ).isTrue(); + } + + @Test + void hasAttributeWithValueContainsSubstring() throws Exception { + final Function quote = string -> "\"" + string + "\""; + final Function beginString = string -> string.substring( 0, 5 ); + final Function centerString = string -> string.substring( 2, 5 ); + final Function endEscapedString = string -> string.substring( 5 ); + + assertAll( // + assertAttribute( "~", centerString, Has::attributeContaining ), // + assertAttribute( "|", beginString, Has::attributeStarting ), // + assertAttribute( "^", beginString.andThen( quote ), Has::attributeBeginning ), // + assertAttribute( "$", endEscapedString.andThen( quote ), Has::attributeEnding ), // + assertAttribute( "*", centerString.andThen( quote ), Has::attributeContainingSubstring ) // + ); + } + + private Executable assertAttribute( final String selectorChar, final Function substring, + final Function> has ) { + final String attributeName = "attributeName"; + final String attributeValue = "attributeValue"; + final String selector = attributeName + selectorChar + "=" + substring.apply( attributeValue ); + when( element.getAttributeValue( attributeName ) ).thenReturn( attributeValue ); + + return () -> assertThat( has.apply( selector ).test( element ) ).isTrue(); + } + + @Test + void hasAttributeWithValue() throws Exception { + final String attributeName = "attributeName"; + final String attributeValue = "attributeValue"; + final String selector = attributeName + "=" + attributeValue; + when( element.getAttributeValue( attributeName ) ).thenReturn( attributeValue ); + + assertThat( Has.attribute( selector ).test( element ) ).isTrue(); + } + + @Test + void hasAttributeWithEscapedValue() throws Exception { + final String attributeName = "attributeName"; + final String attributeValue = "attributeValue"; + final String selector = attributeName + "=\"" + attributeValue + "\""; + when( element.getAttributeValue( attributeName ) ).thenReturn( attributeValue ); + + assertThat( Has.attribute( selector ).test( element ) ).isTrue(); + } + + @Test + void hasAttribute() throws Exception { + final String attributeName = "attributeName"; + final String selector = attributeName; + when( element.getAttributeValue( attributeName ) ).thenReturn( "true" ); + + assertThat( Has.attribute( selector ).test( element ) ).isTrue(); + } + + @Test + void hasLinkTextAsAttribute() throws Exception { + final String value = "value"; + when( identifyingAttribues.getType() ).thenReturn( "a" ); + when( attributes.get( TEXT ) ).thenReturn( value ); + when( element.getAttributes() ).thenReturn( attributes ); + when( element.getIdentifyingAttributes() ).thenReturn( identifyingAttribues ); + + assertThat( Has.linkText( value ).test( element ) ).isTrue(); + } + + @Test + void hasLinkTextAsIdentifyingAttribute() throws Exception { + final String value = "value"; + when( identifyingAttribues.get( TEXT ) ).thenReturn( value ); + when( identifyingAttribues.getType() ).thenReturn( "not a" ); + when( element.getIdentifyingAttributes() ).thenReturn( identifyingAttribues ); + + assertThat( Has.linkText( value ).test( element ) ).isTrue(); + } + + @Test + void hasPartialLinkText() throws Exception { + final String partialLinkText = "partial link"; + final String linkText = partialLinkText + "prefix"; + when( identifyingAttribues.getType() ).thenReturn( "a" ); + when( element.getAttributeValue( TEXT ) ).thenReturn( linkText ); + when( element.getIdentifyingAttributes() ).thenReturn( identifyingAttribues ); + + assertThat( Has.partialLinkText( partialLinkText ).test( element ) ).isEqualTo( true ); + } + + @Test + void hasNoPartialLinkText() throws Exception { + final String partialLinkText = "partial link"; + final String linkText = partialLinkText + "prefix"; + final String notMatchingLink = "not matching"; + when( identifyingAttribues.getType() ).thenReturn( "a" ); + when( element.getAttributeValue( TEXT ) ).thenReturn( linkText ); + when( element.getIdentifyingAttributes() ).thenReturn( identifyingAttribues ); + + assertThat( Has.partialLinkText( notMatchingLink ).test( element ) ).isEqualTo( false ); + } +}