diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml index 82eff8f0..909b3eda 100644 --- a/.github/workflows/ui-tests.yml +++ b/.github/workflows/ui-tests.yml @@ -20,13 +20,13 @@ jobs: include: - targetPlatform: StandaloneOSX buildMethod: MacBuilder.BuildForAltTester - buildPath: MacOS + buildPath: sample/Builds/MacOS - targetPlatform: StandaloneWindows64 buildMethod: WindowsBuilder.BuildForAltTester - buildPath: Windows64 + buildPath: sample/Builds/Windows64 - targetPlatform: Android buildMethod: MobileBuilder.BuildForAltTester - buildPath: Android + buildPath: sample/Builds/Android steps: - uses: actions/checkout@v3 with: @@ -47,15 +47,14 @@ jobs: with: targetPlatform: ${{ matrix.targetPlatform }} projectPath: sample - buildMethod: ${{ matrix.buildMethod }} - customParameters: -logFile logFile.log -quit -batchmode - name: List build directory - run: ls -R sample/Builds/ + run: ls -R ./ - name: Upload artifact uses: actions/upload-artifact@v4 + if: always() with: name: Build-${{ matrix.targetPlatform }} - path: sample/Builds/${{ matrix.buildPath }} + path: ${{ matrix.buildPath }} test: name: Run ${{ matrix.targetPlatform }} UI tests 🧪 needs: build @@ -70,7 +69,10 @@ jobs: test_script: ./test_windows.ps1 - targetPlatform: Android runs-on: [ self-hosted, macOS ] - test_script: browserstack-sdk pytest -s ./test/test_android.py + test_script: browserstack-sdk pytest -s ./test/test_android.py --browserstack.config "browserstack.android.yml" + - targetPlatform: iOS + runs-on: [ self-hosted, macOS ] + test_script: browserstack-sdk pytest -s ./test/test_ios.py --browserstack.config "browserstack.ios.yml" concurrency: group: test-${{ matrix.targetPlatform }} runs-on: ${{ matrix.runs-on }} diff --git a/sample/Tests/Payload.ipa b/sample/Tests/Payload.ipa new file mode 100644 index 00000000..8397d92d Binary files /dev/null and b/sample/Tests/Payload.ipa differ diff --git a/sample/Tests/browserstack.yml b/sample/Tests/browserstack.android.yml similarity index 99% rename from sample/Tests/browserstack.yml rename to sample/Tests/browserstack.android.yml index daf1f5a9..dcdce796 100644 --- a/sample/Tests/browserstack.yml +++ b/sample/Tests/browserstack.android.yml @@ -13,7 +13,7 @@ # Set 'projectName' to the name of your project. Example, Marketing Website projectName: Unity Sample App # Set `buildName` as the name of the job / testsuite being run -buildName: browserstack build +buildName: Android build # `buildIdentifier` is a unique id to differentiate every execution that gets appended to # buildName. Choose your buildIdentifier format from the available expressions: # ${BUILD_NUMBER} (Default): Generates an incremental counter with every execution diff --git a/sample/Tests/browserstack.ios.yml b/sample/Tests/browserstack.ios.yml new file mode 100644 index 00000000..4d39ce54 --- /dev/null +++ b/sample/Tests/browserstack.ios.yml @@ -0,0 +1,73 @@ +# ============================= +# Set BrowserStack Credentials +# ============================= +# Add your BrowserStack userName and acccessKey here or set BROWSERSTACK_USERNAME and +# BROWSERSTACK_ACCESS_KEY as env variables +#userName: BROWSERSTACK_USERNAME +#accessKey: BROWSERSTACK_ACCESS_KEY + +# ====================== +# BrowserStack Reporting +# ====================== +# The following capabilities are used to set up reporting on BrowserStack: +# Set 'projectName' to the name of your project. Example, Marketing Website +projectName: Unity Sample App +# Set `buildName` as the name of the job / testsuite being run +buildName: iOS build +# `buildIdentifier` is a unique id to differentiate every execution that gets appended to +# buildName. Choose your buildIdentifier format from the available expressions: +# ${BUILD_NUMBER} (Default): Generates an incremental counter with every execution +# ${DATE_TIME}: Generates a Timestamp with every execution. Eg. 05-Nov-19:30 +# Read more about buildIdentifiers here -> https://www.browserstack.com/docs/automate/selenium/organize-tests +buildIdentifier: '#${BUILD_NUMBER}' # Supports strings along with either/both ${expression} +framework: pytest +source: pytest-browserstack:sample-sdk:v1.0 + +# Set `app` to define the app that is to be used for testing. +# It can either take the id of any uploaded app or the path of the app directly. +#app: ./WikipediaSample.apk +app: ./Payload.ipa #For running local tests + +# ======================================= +# Platforms (Browsers / Devices to test) +# ======================================= +# Platforms object contains all the browser / device combinations you want to test on. +# Entire list available here -> (https://www.browserstack.com/list-of-browsers-and-platforms/automate) + +platforms: + - platformName: ios + deviceName: iPhone 14 Pro Max + platformVersion: 16 + +# ======================= +# Parallels per Platform +# ======================= +# The number of parallel threads to be used for each platform set. +# BrowserStack's SDK runner will select the best strategy based on the configured value +# +# Example 1 - If you have configured 3 platforms and set `parallelsPerPlatform` as 2, a total of 6 (2 * 3) parallel threads will be used on BrowserStack +# +# Example 2 - If you have configured 1 platform and set `parallelsPerPlatform` as 5, a total of 5 (1 * 5) parallel threads will be used on BrowserStack +parallelsPerPlatform: 1 + +# ========================================== +# BrowserStack Local +# (For localhost, staging/private websites) +# ========================================== +# Set browserStackLocal to true if your website under test is not accessible publicly over the internet +# Learn more about how BrowserStack Local works here -> https://www.browserstack.com/docs/automate/selenium/local-testing-introduction +browserstackLocal: true # (Default false) +browserStackLocalOptions: + #Options to be passed to BrowserStack local in-case of advanced configurations + # localIdentifier: # (Default: null) Needed if you need to run multiple instances of local. + forceLocal: true # (Default: false) Set to true if you need to resolve all your traffic via BrowserStack Local tunnel. +# Entire list of arguments available here -> https://www.browserstack.com/docs/automate/selenium/manage-incoming-connections + +# =================== +# Debugging features +# =================== +debug: false # # Set to true if you need screenshots for every selenium command ran +networkLogs: false # Set to true to enable HAR logs capturing +consoleLogs: errors # Remote browser's console debug levels to be printed (Default: errors) +# Available options are `disable`, `errors`, `warnings`, `info`, `verbose` (Default: errors) +acceptInsecureCerts: true diff --git a/sample/Tests/test/test_ios.py b/sample/Tests/test/test_ios.py new file mode 100644 index 00000000..ce5f3aa9 --- /dev/null +++ b/sample/Tests/test/test_ios.py @@ -0,0 +1,103 @@ +import sys +import time +import unittest +from pathlib import Path + +from appium import webdriver +from appium.options.ios import XCUITestOptions +from appium.webdriver.common.appiumby import AppiumBy +from appium.webdriver.webdriver import WebDriver +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC + +from alttester import AltDriver, By + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent / 'src')) +from fetch_otp import EMAIL, fetch_code + +# To run this test on an actual Android device: appium --base-path /wd/hub --allow-insecure chromedriver_autodownload +class TestBase(unittest.TestCase): + altdriver = None + appium_driver = None + + @classmethod + def setUpClass(cls): + # https://appium.github.io/appium-xcuitest-driver/latest/preparation/real-device-config/ + options = XCUITestOptions() + options.app = "./Payload.ipa" + options.show_xcode_log = True + options.xcode_org_id = "APPLE_TEAM_ID" # Replace with Apple Team ID + options.auto_accept_alerts = True + + cls.appium_driver = webdriver.Remote('https://hub-cloud.browserstack.com/wd/hub/', options=options) + + time.sleep(10) + cls.altdriver = AltDriver() + + @classmethod + def tearDownClass(cls): + print("\nEnding") + cls.altdriver.stop() + cls.appium_driver.quit() + + def test_1_pkce_login(self): + # Select use PKCE auth + self.altdriver.find_object(By.NAME, "PKCE").tap() + + # Wait for unauthenticated screen + self.altdriver.wait_for_current_scene_to_be("UnauthenticatedScene") + + # Login + loginBtn = self.altdriver.wait_for_object(By.NAME, "LoginBtn") + loginBtn.tap() + + driver = self.appium_driver + + # Wait for the ASWebAuthenticationSession context to appear + WebDriverWait(driver, 30).until(lambda d: len(d.contexts) > 2) + contexts = driver.contexts + + target_context = None + + # Since it's unclear which WebView context contains the email field on iOS, + # we need to iterate through each context to identify the correct one. + for context in contexts: + if context == "NATIVE_APP": + continue + + driver.switch_to.context(context) + + try: + # Attempt to find the email input field + email_field = WebDriverWait(driver, 5).until( + EC.presence_of_element_located((AppiumBy.XPATH, "//input[@name='address']")) + ) + # Found email + target_context = context + + email_field.send_keys("imx.game.sdk.demo@gmail.com") + submit_button = driver.find_element(by=AppiumBy.XPATH, value="//form/div/div/div[2]/button") + submit_button.click() + + time.sleep(10) # Wait for OTP + + code = fetch_gmail_code() + assert code, "Failed to fetch OTP from Gmail" + print(f"Successfully fetched OTP: {code}") + + # Unlike on Android, each digit must be entered into a separate input field on iOS. + for i, digit in enumerate(code): + otp_field = driver.find_element(by=AppiumBy.XPATH, value=f"//div[@id='passwordless_container']/div[{i + 1}]/input") + otp_field.send_keys(digit) + + # Wait for authenticated screen + self.altdriver.wait_for_current_scene_to_be("AuthenticatedScene") + + break + except: + # If the field is not found, continue to the next context + print(f"Email field not found in context: {context}") + + # If target context was not found, raise an error + if not target_context: + raise Exception("Could not find the email field in any webview context.") \ No newline at end of file