From 7f7b9fb5b6af8fd7a743bf127bd019afe36c532a Mon Sep 17 00:00:00 2001 From: Uladzislau Arlouski Date: Mon, 17 Jan 2022 17:58:32 +0300 Subject: [PATCH] [plugin-mobile-app] Add step to set slider value --- .../mobileapp/action/TouchActions.java | 28 ++-- .../vividus/mobileapp/steps/ElementSteps.java | 142 +++++++++++++++++- .../action/MobileAppElementActions.java | 11 +- .../mobileapp/steps/ElementStepsTests.java | 117 ++++++++++++++- .../system/mobile_app/locators/android.table | 1 + .../system/mobile_app/locators/ios.table | 1 + .../mobile_app/android/environment.properties | 1 + .../mobile_app/ios/environment.properties | 1 + .../mobile_app/MobileAppStepsTests.story | 15 ++ 9 files changed, 301 insertions(+), 16 deletions(-) diff --git a/vividus-plugin-mobile-app/src/main/java/org/vividus/mobileapp/action/TouchActions.java b/vividus-plugin-mobile-app/src/main/java/org/vividus/mobileapp/action/TouchActions.java index 9a20dd3178..fb16d6b839 100644 --- a/vividus-plugin-mobile-app/src/main/java/org/vividus/mobileapp/action/TouchActions.java +++ b/vividus-plugin-mobile-app/src/main/java/org/vividus/mobileapp/action/TouchActions.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2021 the original author or authors. + * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -198,6 +198,22 @@ public void performVerticalSwipe(int startY, int endY, Rectangle swipeArea, Dura mobileApplicationConfiguration.getSwipeVerticalXPosition(), swipeArea.getPoint()), swipeDuration); } + /** + * Performs swipe by coordinates with duration + * + * @param coordinates the {@link SwipeCoordinates} + * @param swipeDuration the swipe duration in ISO 8601 format + */ + public void swipe(SwipeCoordinates coordinates, Duration swipeDuration) + { + newTouchActions() + .press(point(coordinates.getStart())) + .waitAction(waitOptions(swipeDuration)) + .moveTo(point(coordinates.getEnd())) + .release() + .perform(); + } + private BufferedImage takeScreenshot() { try @@ -210,16 +226,6 @@ private BufferedImage takeScreenshot() } } - private void swipe(SwipeCoordinates coordinates, Duration swipeDuration) - { - newTouchActions() - .press(point(coordinates.getStart())) - .waitAction(waitOptions(swipeDuration)) - .moveTo(point(coordinates.getEnd())) - .release() - .perform(); - } - private static Point getCenter(WebElement element) { Point upperLeft = element.getLocation(); diff --git a/vividus-plugin-mobile-app/src/main/java/org/vividus/mobileapp/steps/ElementSteps.java b/vividus-plugin-mobile-app/src/main/java/org/vividus/mobileapp/steps/ElementSteps.java index 1abc62deb8..425f71513f 100644 --- a/vividus-plugin-mobile-app/src/main/java/org/vividus/mobileapp/steps/ElementSteps.java +++ b/vividus-plugin-mobile-app/src/main/java/org/vividus/mobileapp/steps/ElementSteps.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2021 the original author or authors. + * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,30 +18,48 @@ import static org.apache.commons.lang3.Validate.isTrue; +import java.time.Duration; import java.util.Map; +import org.apache.commons.lang3.StringUtils; import org.jbehave.core.annotations.When; +import org.openqa.selenium.Dimension; +import org.openqa.selenium.Point; +import org.openqa.selenium.WebElement; import org.openqa.selenium.remote.RemoteWebElement; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.vividus.mobileapp.action.TouchActions; +import org.vividus.mobileapp.model.SwipeCoordinates; import org.vividus.monitor.TakeScreenshotOnFailure; import org.vividus.selenium.WebDriverUtil; import org.vividus.selenium.manager.IGenericWebDriverManager; import org.vividus.steps.ui.validation.IBaseValidations; import org.vividus.ui.action.JavascriptActions; import org.vividus.ui.action.search.Locator; +import org.vividus.ui.mobile.action.MobileAppElementActions; @TakeScreenshotOnFailure public class ElementSteps { + private static final Logger LOGGER = LoggerFactory.getLogger(ElementSteps.class); + private static final double PERCENT = 100.0; + private final IGenericWebDriverManager genericWebDriverManager; private final JavascriptActions javascriptActions; private final IBaseValidations baseValidations; + private final TouchActions touchActions; + private final MobileAppElementActions mobileAppElementActions; public ElementSteps(JavascriptActions javascriptActions, IGenericWebDriverManager genericWebDriverManager, - IBaseValidations baseValidations) + IBaseValidations baseValidations, TouchActions touchActions, + MobileAppElementActions mobileAppElementActions) { this.javascriptActions = javascriptActions; this.genericWebDriverManager = genericWebDriverManager; this.baseValidations = baseValidations; + this.touchActions = touchActions; + this.mobileAppElementActions = mobileAppElementActions; } /** @@ -71,6 +89,126 @@ public void selectPickerWheelValue(PickerWheelDirection direction, double offset "offset", offset))); } + /** + * Todo + * + * @param locator locator to find a slider + * @param number target value to slide to + */ + @SuppressWarnings("MagicNumber") + @When("I set value of slider located `$locator` to `$number`") + public void setSliderValue(Locator locator, int number) + { + baseValidations.assertElementExists("The slider", locator).map(Slider::new).ifPresent(slider -> + { + int initialPosition = slider.getPosition(); + if (initialPosition == number) + { + logSliderPosition(number); + return; + } + + int sliderPosition = swipeSlider(slider, initialPosition, number); + + if (sliderPosition == number) + { + logSliderPosition(number); + return; + } + + int diff = Math.abs(number - sliderPosition); + + int previousSlide = number; + int previousDiff = diff; + int swipeLimit = 5; + + do + { + int adjustment = diff == 1 ? 1 : diff / 2; + int position = sliderPosition < number ? previousSlide + adjustment : previousSlide - adjustment; + previousSlide = position; + + sliderPosition = swipeSlider(slider, sliderPosition, position); + diff = Math.abs(number - sliderPosition); + + if (previousDiff == diff || diff == 0) + { + break; + } + + previousDiff = diff; + + swipeLimit -= 1; + } + while (swipeLimit > 0); + + logSliderPosition(number); + }); + } + + private static void logSliderPosition(int sliderPosition) + { + LOGGER.atInfo().addArgument(sliderPosition).log("Set slider value to '{}'"); + } + + private int swipeSlider(Slider slider, int currentPosition, int targetPosition) + { + SwipeCoordinates coordinates = getSwipeCoordinates(slider.getLocation(), slider.getSize(), + currentPosition, targetPosition); + touchActions.swipe(coordinates, Duration.ofSeconds(1)); + return slider.getPosition(); + } + + private static SwipeCoordinates getSwipeCoordinates(Point sliderLocation, Dimension sliderSize, int currentValue, + int targetValue) + { + int sliderBarOffsetLeft = sliderLocation.getX(); + int sliderBarWidth = sliderSize.getWidth(); + int sliderBarCenterY = sliderLocation.getY() + sliderSize.getHeight() / 2; + + int startX = (int) (sliderBarOffsetLeft + sliderBarWidth * (currentValue / PERCENT) + 0); + int startY = sliderBarCenterY; + int endX = (int) (sliderBarOffsetLeft + sliderBarWidth * (targetValue / PERCENT)); + int endY = sliderBarCenterY; + + return new SwipeCoordinates(startX, startY, endX, endY); + } + + private final class Slider + { + private final Point location; + private final Dimension size; + private final WebElement target; + + private Slider(WebElement target) + { + String tag = mobileAppElementActions.getTagName(target); + String sliderTag = genericWebDriverManager.isIOSNativeApp() ? "XCUIElementTypeSlider" + : "android.widget.SeekBar"; + isTrue(sliderTag.equals(tag), "The slider element must be '%s', but got '%s'", sliderTag, tag); + this.location = target.getLocation(); + this.size = target.getSize(); + this.target = target; + } + + private Point getLocation() + { + return location; + } + + private Dimension getSize() + { + return size; + } + + private int getPosition() + { + String text = mobileAppElementActions.getElementText(target); + return genericWebDriverManager.isIOSNativeApp() ? Integer.parseInt(StringUtils.removeEnd(text, "%")) + : Double.valueOf(text).intValue(); + } + } + public enum PickerWheelDirection { NEXT, PREVIOUS; diff --git a/vividus-plugin-mobile-app/src/main/java/org/vividus/ui/mobile/action/MobileAppElementActions.java b/vividus-plugin-mobile-app/src/main/java/org/vividus/ui/mobile/action/MobileAppElementActions.java index 876fa05ec5..e0544cf7be 100644 --- a/vividus-plugin-mobile-app/src/main/java/org/vividus/ui/mobile/action/MobileAppElementActions.java +++ b/vividus-plugin-mobile-app/src/main/java/org/vividus/ui/mobile/action/MobileAppElementActions.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2021 the original author or authors. + * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,6 +29,15 @@ public MobileAppElementActions(GenericWebDriverManager genericWebDriverManager) this.genericWebDriverManager = genericWebDriverManager; } + public String getTagName(WebElement element) + { + /* + * For Android platform the WebElement.getTagName() method returns content description of the element: + * https://github.com/appium/appium-uiautomator2-server/blob/master/app/src/main/java/io/appium/uiautomator2/model/UiObject2Element.java#L58 + */ + return genericWebDriverManager.isAndroidNativeApp() ? element.getDomAttribute("class") : element.getTagName(); + } + @Override public String getElementText(WebElement element) { diff --git a/vividus-plugin-mobile-app/src/test/java/org/vividus/mobileapp/steps/ElementStepsTests.java b/vividus-plugin-mobile-app/src/test/java/org/vividus/mobileapp/steps/ElementStepsTests.java index ee3f0982b4..231e657171 100644 --- a/vividus-plugin-mobile-app/src/test/java/org/vividus/mobileapp/steps/ElementStepsTests.java +++ b/vividus-plugin-mobile-app/src/test/java/org/vividus/mobileapp/steps/ElementStepsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2021 the original author or authors. + * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,37 +18,60 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; +import java.time.Duration; +import java.util.List; import java.util.Map; import java.util.Optional; +import com.github.valfirst.slf4jtest.TestLogger; +import com.github.valfirst.slf4jtest.TestLoggerFactory; +import com.github.valfirst.slf4jtest.TestLoggerFactoryExtension; + import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.openqa.selenium.Dimension; +import org.openqa.selenium.Point; +import org.openqa.selenium.WebElement; import org.openqa.selenium.remote.RemoteWebElement; +import org.vividus.mobileapp.action.TouchActions; +import org.vividus.mobileapp.model.SwipeCoordinates; import org.vividus.mobileapp.steps.ElementSteps.PickerWheelDirection; import org.vividus.selenium.manager.IGenericWebDriverManager; import org.vividus.steps.ui.validation.IBaseValidations; import org.vividus.ui.action.JavascriptActions; import org.vividus.ui.action.search.Locator; +import org.vividus.ui.mobile.action.MobileAppElementActions; -@ExtendWith(MockitoExtension.class) +@ExtendWith({ MockitoExtension.class, TestLoggerFactoryExtension.class }) class ElementStepsTests { private static final String PICKER_WHEEL = "Picker wheel"; private static final double OFFSET = 0.1; + private static final String IOS_SLIDER = "XCUIElementTypeSlider"; @Mock private IGenericWebDriverManager genericWebDriverManager; @Mock private JavascriptActions javascriptActions; @Mock private IBaseValidations baseValidations; @Mock private Locator locator; + @Mock private TouchActions touchActions; + @Mock private MobileAppElementActions mobileAppElementActions; @InjectMocks private ElementSteps elementSteps; + private final TestLogger logger = TestLoggerFactory.getTestLogger(ElementSteps.class); + @Test void shouldSelectPickerWheelValue() { @@ -89,4 +112,94 @@ void shouldNotSelectPickerWheelValueOnAndroid() () -> elementSteps.selectPickerWheelValue(PickerWheelDirection.NEXT, OFFSET, locator)); assertEquals("Picker wheel selection is supported only for iOS platform", exception.getMessage()); } + + @ParameterizedTest + @CsvSource({ + "true , 0% , 50%, XCUIElementTypeSlider", + "false, 0.0, 50.0, android.widget.SeekBar" + }) + void shouldSetSliderValueWithoutAdjustmentIfGetExactValueAfterFirstSliderSwipe(boolean iosApp, + String initialSliderValue, String sliderValue, String sliderTag) + { + WebElement slider = mockSlider(); + when(mobileAppElementActions.getTagName(slider)).thenReturn(sliderTag); + when(genericWebDriverManager.isIOSNativeApp()).thenReturn(iosApp); + when(mobileAppElementActions.getElementText(slider)).thenReturn(initialSliderValue).thenReturn(sliderValue); + + elementSteps.setSliderValue(locator, 50); + + ArgumentCaptor coordsCaptor = ArgumentCaptor.forClass(SwipeCoordinates.class); + verify(touchActions).swipe(coordsCaptor.capture(), eq(Duration.ofSeconds(1))); + verifyCoordinates(coordsCaptor.getValue(), 77, 384, 680); + } + + @Test + void shouldSetSliderValueWithNoExactAdjustment() + { + WebElement slider = mockSlider(); + when(mobileAppElementActions.getTagName(slider)).thenReturn(IOS_SLIDER); + when(genericWebDriverManager.isIOSNativeApp()).thenReturn(true); + when(mobileAppElementActions.getElementText(slider)).thenReturn("10%").thenReturn("57%").thenReturn("49%") + .thenReturn("51%"); + + elementSteps.setSliderValue(locator, 50); + + ArgumentCaptor coordsCaptor = ArgumentCaptor.forClass(SwipeCoordinates.class); + verify(touchActions, times(3)).swipe(coordsCaptor.capture(), eq(Duration.ofSeconds(1))); + List coords = coordsCaptor.getAllValues(); + verifyCoordinates(coords.get(0), 138, 384, 680); + verifyCoordinates(coords.get(1), 426, 365, 680); + verifyCoordinates(coords.get(2), 377, 371, 680); + } + + @Test + void shouldNotSetSliderValueIfItIsAlreadyEqualToTargetValue() + { + WebElement slider = mockSlider(); + when(mobileAppElementActions.getTagName(slider)).thenReturn(IOS_SLIDER); + when(genericWebDriverManager.isIOSNativeApp()).thenReturn(true); + when(mobileAppElementActions.getElementText(slider)).thenReturn("15%"); + + elementSteps.setSliderValue(locator, 15); + + verifyNoInteractions(touchActions); + } + + @Test + void shouldSetSliderValueWithExactAdjustment() + { + WebElement slider = mockSlider(); + when(mobileAppElementActions.getTagName(slider)).thenReturn(IOS_SLIDER); + when(genericWebDriverManager.isIOSNativeApp()).thenReturn(true); + when(mobileAppElementActions.getElementText(slider)).thenReturn("0%").thenReturn("43%") + .thenReturn("60%").thenReturn("50%"); + + elementSteps.setSliderValue(locator, 50); + + ArgumentCaptor coordsCaptor = ArgumentCaptor.forClass(SwipeCoordinates.class); + verify(touchActions, times(3)).swipe(coordsCaptor.capture(), eq(Duration.ofSeconds(1))); + List coords = coordsCaptor.getAllValues(); + verifyCoordinates(coords.get(0), 77, 384, 680); + verifyCoordinates(coords.get(1), 341, 402, 680); + verifyCoordinates(coords.get(2), 445, 371, 680); + } + + private static void verifyCoordinates(SwipeCoordinates coordinates, int startX, int endX, int y) + { + assertEquals(new Point(startX, y), coordinates.getStart()); + assertEquals(new Point(endX, y), coordinates.getEnd()); + } + + private WebElement mockSlider() + { + WebElement sliderElement = mock(WebElement.class); + Point location = new Point(77, 620); + when(sliderElement.getLocation()).thenReturn(location); + Dimension size = new Dimension(614, 120); + when(sliderElement.getSize()).thenReturn(size); + + when(baseValidations.assertElementExists("The slider", locator)).thenReturn(Optional.of(sliderElement)); + + return sliderElement; + } } diff --git a/vividus-tests/src/main/resources/data/tables/system/mobile_app/locators/android.table b/vividus-tests/src/main/resources/data/tables/system/mobile_app/locators/android.table index 55cfb12ba3..796c95a592 100644 --- a/vividus-tests/src/main/resources/data/tables/system/mobile_app/locators/android.table +++ b/vividus-tests/src/main/resources/data/tables/system/mobile_app/locators/android.table @@ -1,6 +1,7 @@ {transformer=FROM_LANDSCAPE} |textElementXpath |//android.widget.TextView | |menuButtonXpath |//android.widget.TextView[@text='Button'] | +|menuSliderXpath |//android.widget.TextView[@text='Slider'] | |menuInputXpath |//android.widget.TextView[@text='Input'] | |nameDisplayXpath |//android.widget.TextView[@text='${text}'] | |menuQrCodeXpath |//android.widget.TextView[@text='QR Code'] | diff --git a/vividus-tests/src/main/resources/data/tables/system/mobile_app/locators/ios.table b/vividus-tests/src/main/resources/data/tables/system/mobile_app/locators/ios.table index e15be89a09..989b81fa76 100644 --- a/vividus-tests/src/main/resources/data/tables/system/mobile_app/locators/ios.table +++ b/vividus-tests/src/main/resources/data/tables/system/mobile_app/locators/ios.table @@ -1,6 +1,7 @@ {transformer=FROM_LANDSCAPE} |textElementXpath |//XCUIElementTypeStaticText | |menuButtonXpath |//XCUIElementTypeButton[@name="Button"] | +|menuSliderXpath |//XCUIElementTypeButton[@name="Slider"] | |menuInputXpath |//XCUIElementTypeButton[@name="Input"] | |nameDisplayXpath |//XCUIElementTypeTextField[@value='${text}'] | |menuQrCodeXpath |//XCUIElementTypeButton[@name="QR Code"] | diff --git a/vividus-tests/src/main/resources/properties/environment/system/saucelabs/mobile_app/android/environment.properties b/vividus-tests/src/main/resources/properties/environment/system/saucelabs/mobile_app/android/environment.properties index 4c20597578..6e753f8c10 100644 --- a/vividus-tests/src/main/resources/properties/environment/system/saucelabs/mobile_app/android/environment.properties +++ b/vividus-tests/src/main/resources/properties/environment/system/saucelabs/mobile_app/android/environment.properties @@ -10,3 +10,4 @@ bdd.variables.global.browser-app=com.android.chrome bdd.variables.global.main-app=com.vividustestapp bdd.variables.global.visibility-attribute=displayed +bdd.variables.global.value-attribute=text diff --git a/vividus-tests/src/main/resources/properties/environment/system/saucelabs/mobile_app/ios/environment.properties b/vividus-tests/src/main/resources/properties/environment/system/saucelabs/mobile_app/ios/environment.properties index a487f326da..f21b542b12 100644 --- a/vividus-tests/src/main/resources/properties/environment/system/saucelabs/mobile_app/ios/environment.properties +++ b/vividus-tests/src/main/resources/properties/environment/system/saucelabs/mobile_app/ios/environment.properties @@ -10,3 +10,4 @@ bdd.variables.global.browser-app=com.apple.mobilesafari bdd.variables.global.main-app=org.reactjs.native.example.vividusTestApp bdd.variables.global.visibility-attribute=visible +bdd.variables.global.value-attribute=value diff --git a/vividus-tests/src/main/resources/story/system/mobile_app/MobileAppStepsTests.story b/vividus-tests/src/main/resources/story/system/mobile_app/MobileAppStepsTests.story index 2a7152f62f..e4e41d54c8 100644 --- a/vividus-tests/src/main/resources/story/system/mobile_app/MobileAppStepsTests.story +++ b/vividus-tests/src/main/resources/story/system/mobile_app/MobileAppStepsTests.story @@ -285,6 +285,21 @@ When I scan barcode from screen and save result to scenario variable `qrCodeLink Then `${qrCodeLink}` is = `https://github.com/vividus-framework/vividus` +Scenario: Verify step: 'When I set value of slider located '$locator' to '$percent' percent' +When I tap on element located `accessibilityId(menuToggler)` +When I tap on element located `xpath()` +When I wait until element located `accessibilityId(zeroToHundredSlider)` appears +When I set value of slider located `accessibilityId(zeroToHundredSlider)` to `15` +When I save `${value-attribute}` attribute value of element located `accessibilityId(zeroToHundredSliderPosition)` to scenario variable `sliderState` +Then `${sliderState}` matches `(14|15|16)` +When I set value of slider located `accessibilityId(zeroToHundredSlider)` to `74` +When I save `${value-attribute}` attribute value of element located `accessibilityId(zeroToHundredSliderPosition)` to scenario variable `sliderState` +Then `${sliderState}` matches `(73|74|75)` +When I set value of slider located `accessibilityId(zeroToHundredSlider)` to `7` +When I save `${value-attribute}` attribute value of element located `accessibilityId(zeroToHundredSliderPosition)` to scenario variable `sliderState` +Then `${sliderState}` matches `(6|7|8)` + + Scenario: Verify step: 'When I long press $key key' Meta: @targetPlatform android