diff --git a/.github/workflows/sdk-sample-test.yml b/.github/workflows/sdk-sample-test.yml new file mode 100644 index 0000000..705e395 --- /dev/null +++ b/.github/workflows/sdk-sample-test.yml @@ -0,0 +1,75 @@ +# Runs the BrowserStack SDK sample against a given commit and reports a status check. +# Trigger: Actions tab -> "pytest-bdd Appium App Automate SDK sample test" -> Run workflow -> paste the PR's full commit SHA. +# Requires repo secrets: BROWSERSTACK_USERNAME, BROWSERSTACK_ACCESS_KEY. +# NOTE (App Automate): the app under test is referenced via `app: bs://...` in browserstack.yml; +# ensure that uploaded app exists on the account whose secrets are used (re-upload + update if expired). +name: pytest-bdd Appium App Automate SDK sample test + +on: + workflow_dispatch: + inputs: + commit_sha: + description: 'The full commit id to build' + required: true + +permissions: + contents: read # checkout + checks: write # github-script creates the status check + +jobs: + sdk-sample: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + max-parallel: 3 + matrix: + os: [ubuntu-latest] + python: ['3.10', '3.11'] + name: pytest-bdd-appium Python ${{ matrix.python }} sample + env: + BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME }} + BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} + defaults: + run: + working-directory: android + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.inputs.commit_sha }} + - name: Mark status check in_progress + uses: actions/github-script@v7 + env: + job_name: pytest-bdd-appium Python ${{ matrix.python }} sample + commit_sha: ${{ github.event.inputs.commit_sha }} + with: + github-token: ${{ github.token }} + script: | + await github.rest.checks.create({ + owner: context.repo.owner, repo: context.repo.repo, + name: process.env.job_name, head_sha: process.env.commit_sha, + status: 'in_progress' + }).catch(e => console.log('check create failed:', e.status)); + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python }} + - name: Install + run: | + pip install -r requirements.txt + - name: Run sample test + run: | + browserstack-sdk pytest tests/test_sample.py + - name: Mark status check completed + if: always() + uses: actions/github-script@v7 + env: + conclusion: ${{ job.status }} + job_name: pytest-bdd-appium Python ${{ matrix.python }} sample + commit_sha: ${{ github.event.inputs.commit_sha }} + with: + github-token: ${{ github.token }} + script: | + await github.rest.checks.create({ + owner: context.repo.owner, repo: context.repo.repo, + name: process.env.job_name, head_sha: process.env.commit_sha, + status: 'completed', conclusion: process.env.conclusion + }).catch(e => console.log('check create failed:', e.status)); diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..160f17f --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +.vscode +__pycache__ +.pytest_cache +.venv +env +local.log +log/ +*.apk +*.ipa diff --git a/README.md b/README.md index 8a077e8..5434b34 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,84 @@ -# pytest-bdd-appium-app-browserstack -We require the following new public repositories under the browserstack GitHub organization to host customer-facing sample projects for the BrowserStack SDK. +# pytest-bdd + Appium with BrowserStack App Automate + +Run Appium (mobile app) tests written with **pytest-bdd** on real devices on +[BrowserStack App Automate](https://app-automate.browserstack.com/) using the +BrowserStack Python SDK. No capability boilerplate in your tests — the SDK reads +`browserstack.yml` and routes each session to the BrowserStack device cloud. + +This repo has two self-contained platform directories: + +``` +android/ Android sample (WikipediaSample.apk) + local (LocalSample.apk) +ios/ iOS sample (BStackSampleApp.ipa) + local (LocalSample.ipa) +``` + +Each directory has its own `browserstack.yml`, `conftest.py` (Appium driver +fixture), `features/` (Gherkin), `tests/` (pytest-bdd step definitions), and +`requirements.txt`. + +## Prerequisites + +- A [BrowserStack](https://www.browserstack.com/) account (username + access key). +- Python 3.8+. +- An application to test. The Android directory is pre-wired to a pre-uploaded + `WikipediaSample.apk` (`bs://...`); the iOS directory uploads + `BStackSampleApp.ipa` from a local path. + +## Setup + +```bash +git clone +cd pytest-bdd-appium-app-browserstack/android # or: cd pytest-bdd-appium-app-browserstack/ios + +python3 -m venv .venv +.venv/bin/pip install -r requirements.txt +``` + +Configure credentials via env vars (recommended) or by editing `browserstack.yml`: + +```bash +export BROWSERSTACK_USERNAME="YOUR_USERNAME" +export BROWSERSTACK_ACCESS_KEY="YOUR_ACCESS_KEY" +``` + +## Run Sample Test (Android) + +From inside `android/`: + +```bash +browserstack-sdk pytest -s tests/ +``` + +This runs the **Wikipedia search** scenario on a real Samsung Galaxy S22 Ultra: +tap "Search Wikipedia", type "BrowserStack", and assert results are returned. +It also runs the **local** scenario (LocalSample.apk over the BrowserStack Local +tunnel — `browserstackLocal: true`). + +To run a single scenario: + +```bash +browserstack-sdk pytest -s tests/test_sample.py +``` + +## Run Sample Test (iOS) + +From inside `ios/`: + +```bash +browserstack-sdk pytest -s tests/ +``` + +## Run Local Test + +The local scenarios (`tests/test_local.py`) exercise BrowserStack Local. With +`browserstackLocal: true` in `browserstack.yml` the SDK starts the Local tunnel +automatically — no separate binary to launch. + +## Notes / Dashboard + +- View runs and shareable session links at + [app-automate.browserstack.com](https://app-automate.browserstack.com/). +- Test Observability (`testObservability: true`) reports also appear at + [observability.browserstack.com](https://observability.browserstack.com/). +- The `app:` value can be a local path (the SDK uploads it and rewrites to + `bs://`) or a pre-uploaded `bs://`. diff --git a/android/LocalSample.apk b/android/LocalSample.apk new file mode 100644 index 0000000..f31c574 Binary files /dev/null and b/android/LocalSample.apk differ diff --git a/android/WikipediaSample.apk b/android/WikipediaSample.apk new file mode 100644 index 0000000..03d19e6 Binary files /dev/null and b/android/WikipediaSample.apk differ diff --git a/android/browserstack.yml b/android/browserstack.yml new file mode 100644 index 0000000..9ebb558 --- /dev/null +++ b/android/browserstack.yml @@ -0,0 +1,49 @@ +# ============================= +# Set BrowserStack Credentials +# ============================= +# Add your BrowserStack userName and accessKey here or set BROWSERSTACK_USERNAME +# and BROWSERSTACK_ACCESS_KEY as env variables. +userName: YOUR_USERNAME +accessKey: YOUR_ACCESS_KEY + +# ====================== +# BrowserStack Reporting +# ====================== +projectName: BrowserStack Samples +buildName: appauto-pytest-bdd-appium +buildIdentifier: '#${BUILD_NUMBER}' +# `framework` lets the SDK instrument pytest-bdd and send test context to BrowserStack. +framework: pytest-bdd + +# ========================================== +# Application under test +# ========================================== +# Pre-uploaded WikipediaSample.apk. Use a local path (./WikipediaSample.apk) to +# Public sample app committed in this repo (relative path); the SDK uploads it at run time. +app: ./WikipediaSample.apk + +# ======================================= +# Platforms (Devices to test) +# ======================================= +platforms: + - deviceName: Samsung Galaxy S22 Ultra + osVersion: "12.0" + platformName: android + +# ======================= +# Parallels per Platform +# ======================= +parallelsPerPlatform: 1 + +source: pytest-bdd:appium-sample-sdk:v1.0 + +# ====================== +# Test Observability +# ====================== +testObservability: true + +# =================== +# Debugging features +# =================== +debug: true +networkLogs: true diff --git a/android/conftest.py b/android/conftest.py new file mode 100644 index 0000000..cb08d64 --- /dev/null +++ b/android/conftest.py @@ -0,0 +1,25 @@ +import os + +import pytest +from appium import webdriver +from appium.options.android import UiAutomator2Options + + +@pytest.fixture(scope="function") +def driver(request): + """Appium driver fixture shared by the pytest-bdd step definitions. + + The BrowserStack SDK injects the app + device capabilities from + browserstack.yml, so an empty UiAutomator2Options object is enough — no + hub URL caps or device caps are set here. When launched with + `browserstack-sdk pytest`, the session is routed to the BrowserStack + cloud automatically. + """ + options = UiAutomator2Options() + options.set_capability("bstack:options", { + "userName": os.environ.get("BROWSERSTACK_USERNAME", "YOUR_USERNAME"), + "accessKey": os.environ.get("BROWSERSTACK_ACCESS_KEY", "YOUR_ACCESS_KEY"), + }) + drv = webdriver.Remote("https://hub.browserstack.com/wd/hub", options=options) + yield drv + drv.quit() diff --git a/android/features/local.feature b/android/features/local.feature new file mode 100644 index 0000000..521dbe7 --- /dev/null +++ b/android/features/local.feature @@ -0,0 +1,9 @@ +Feature: BrowserStack App Automate local test + As a BrowserStack user + I want the local sample app to reach a service over the BrowserStack Local tunnel + So that I can verify BrowserStack Local tunnelling works for mobile apps + + Scenario: Reach the local endpoint from the device + Given I have launched the local sample app + When I trigger the local network test + Then the app reports it is up and running diff --git a/android/features/sample.feature b/android/features/sample.feature new file mode 100644 index 0000000..8ecb7de --- /dev/null +++ b/android/features/sample.feature @@ -0,0 +1,9 @@ +Feature: BrowserStack App Automate sample test + As a BrowserStack user + I want to search Wikipedia in the WikipediaSample app + So that I can verify search results are returned + + Scenario: Search Wikipedia for BrowserStack + Given I have launched the Wikipedia sample app + When I search Wikipedia for "BrowserStack" + Then search results are displayed diff --git a/android/requirements.txt b/android/requirements.txt new file mode 100644 index 0000000..ef0be59 --- /dev/null +++ b/android/requirements.txt @@ -0,0 +1,6 @@ +Appium-Python-Client +selenium>=3.14 +pytest==7.4.4 +pytest-bdd +browserstack-local +browserstack-sdk @ https://sdk-assets.browserstack.com/python/browserstack_sdk-latest.tar.gz diff --git a/android/tests/test_local.py b/android/tests/test_local.py new file mode 100644 index 0000000..6117be8 --- /dev/null +++ b/android/tests/test_local.py @@ -0,0 +1,29 @@ +import os + +from pytest_bdd import scenario, given, when, then +from appium.webdriver.common.appiumby import AppiumBy + +FEATURES = os.path.join(os.path.dirname(os.path.dirname(__file__)), "features") + + +@scenario(os.path.join(FEATURES, "local.feature"), "Reach the local endpoint from the device") +def test_local(): + """LocalSample.apk over the BrowserStack Local tunnel (browserstackLocal: true).""" + + +@given("I have launched the local sample app") +def launch_local_app(driver): + # The SDK launches the local sample app from browserstack.yml. + pass + + +@when("I trigger the local network test") +def trigger_local_test(driver): + driver.find_element( + AppiumBy.ID, "com.example.android.basicnetworking:id/test_action").click() + + +@then("the app reports it is up and running") +def app_up_and_running(driver): + texts = driver.find_elements(AppiumBy.CLASS_NAME, "android.widget.TextView") + assert any("Up and running" in (t.text or "") for t in texts) diff --git a/android/tests/test_sample.py b/android/tests/test_sample.py new file mode 100644 index 0000000..51577c2 --- /dev/null +++ b/android/tests/test_sample.py @@ -0,0 +1,32 @@ +import os +import time + +from pytest_bdd import scenario, given, when, then, parsers +from appium.webdriver.common.appiumby import AppiumBy + +FEATURES = os.path.join(os.path.dirname(os.path.dirname(__file__)), "features") + + +@scenario(os.path.join(FEATURES, "sample.feature"), "Search Wikipedia for BrowserStack") +def test_search_wikipedia(): + """WikipediaSample.apk: search Wikipedia and assert results appear.""" + + +@given("I have launched the Wikipedia sample app") +def launch_app(driver): + # The SDK launches the app (WikipediaSample.apk) from browserstack.yml. + driver.find_element(AppiumBy.ACCESSIBILITY_ID, "Search Wikipedia").click() + + +@when(parsers.parse('I search Wikipedia for "{query}"')) +def search_wikipedia(driver, query): + search = driver.find_element( + AppiumBy.ID, "org.wikipedia.alpha:id/search_src_text") + search.send_keys(query) + time.sleep(5) + + +@then("search results are displayed") +def results_displayed(driver): + results = driver.find_elements(AppiumBy.CLASS_NAME, "android.widget.TextView") + assert len(results) > 0 diff --git a/ios/BStackSampleApp.ipa b/ios/BStackSampleApp.ipa new file mode 100644 index 0000000..c1891b8 Binary files /dev/null and b/ios/BStackSampleApp.ipa differ diff --git a/ios/LocalSample.ipa b/ios/LocalSample.ipa new file mode 100644 index 0000000..a937349 Binary files /dev/null and b/ios/LocalSample.ipa differ diff --git a/ios/browserstack.yml b/ios/browserstack.yml new file mode 100644 index 0000000..fa31559 --- /dev/null +++ b/ios/browserstack.yml @@ -0,0 +1,45 @@ +# ============================= +# Set BrowserStack Credentials +# ============================= +userName: YOUR_USERNAME +accessKey: YOUR_ACCESS_KEY + +# ====================== +# BrowserStack Reporting +# ====================== +projectName: BrowserStack Samples +buildName: appauto-pytest-bdd-appium +buildIdentifier: '#${BUILD_NUMBER}' +framework: pytest-bdd + +# ========================================== +# Application under test +# ========================================== +# Public sample app committed in this repo (relative path); the SDK uploads it at run time. +app: ./BStackSampleApp.ipa + +# ======================================= +# Platforms (Devices to test) +# ======================================= +platforms: + - deviceName: iPhone 14 Pro + osVersion: "16" + platformName: ios + +# ======================= +# Parallels per Platform +# ======================= +parallelsPerPlatform: 1 + +source: pytest-bdd:appium-sample-sdk:v1.0 + +# ====================== +# Test Observability +# ====================== +testObservability: true + +# =================== +# Debugging features +# =================== +debug: true +networkLogs: true diff --git a/ios/conftest.py b/ios/conftest.py new file mode 100644 index 0000000..75c9ac2 --- /dev/null +++ b/ios/conftest.py @@ -0,0 +1,22 @@ +import os + +import pytest +from appium import webdriver +from appium.options.ios import XCUITestOptions + + +@pytest.fixture(scope="function") +def driver(request): + """Appium driver fixture shared by the pytest-bdd step definitions. + + The BrowserStack SDK injects the app + device capabilities from + browserstack.yml, so an empty XCUITestOptions object is enough. + """ + options = XCUITestOptions() + options.set_capability("bstack:options", { + "userName": os.environ.get("BROWSERSTACK_USERNAME", "YOUR_USERNAME"), + "accessKey": os.environ.get("BROWSERSTACK_ACCESS_KEY", "YOUR_ACCESS_KEY"), + }) + drv = webdriver.Remote("https://hub.browserstack.com/wd/hub", options=options) + yield drv + drv.quit() diff --git a/ios/features/local.feature b/ios/features/local.feature new file mode 100644 index 0000000..521dbe7 --- /dev/null +++ b/ios/features/local.feature @@ -0,0 +1,9 @@ +Feature: BrowserStack App Automate local test + As a BrowserStack user + I want the local sample app to reach a service over the BrowserStack Local tunnel + So that I can verify BrowserStack Local tunnelling works for mobile apps + + Scenario: Reach the local endpoint from the device + Given I have launched the local sample app + When I trigger the local network test + Then the app reports it is up and running diff --git a/ios/features/sample.feature b/ios/features/sample.feature new file mode 100644 index 0000000..5fe35a0 --- /dev/null +++ b/ios/features/sample.feature @@ -0,0 +1,9 @@ +Feature: BrowserStack App Automate sample test + As a BrowserStack user + I want to enter text in the BStack sample app + So that I can verify the text output echoes my input + + Scenario: Enter text and verify the output + Given I have launched the BStack sample app + When I enter the text "hello@browserstack.com" + Then the output shows "hello@browserstack.com" diff --git a/ios/requirements.txt b/ios/requirements.txt new file mode 100644 index 0000000..ef0be59 --- /dev/null +++ b/ios/requirements.txt @@ -0,0 +1,6 @@ +Appium-Python-Client +selenium>=3.14 +pytest==7.4.4 +pytest-bdd +browserstack-local +browserstack-sdk @ https://sdk-assets.browserstack.com/python/browserstack_sdk-latest.tar.gz diff --git a/ios/tests/test_local.py b/ios/tests/test_local.py new file mode 100644 index 0000000..0456f73 --- /dev/null +++ b/ios/tests/test_local.py @@ -0,0 +1,28 @@ +import os + +from pytest_bdd import scenario, given, when, then +from appium.webdriver.common.appiumby import AppiumBy + +FEATURES = os.path.join(os.path.dirname(os.path.dirname(__file__)), "features") + + +@scenario(os.path.join(FEATURES, "local.feature"), "Reach the local endpoint from the device") +def test_local(): + """LocalSample.ipa over the BrowserStack Local tunnel (browserstackLocal: true).""" + + +@given("I have launched the local sample app") +def launch_local_app(driver): + pass + + +@when("I trigger the local network test") +def trigger_local_test(driver): + driver.find_element(AppiumBy.ACCESSIBILITY_ID, "TestBrowserStackLocal").click() + + +@then("the app reports it is up and running") +def app_up_and_running(driver): + result = driver.find_element( + AppiumBy.ACCESSIBILITY_ID, "ResultBrowserStackLocal").text + assert "up and running" in (result or "").lower() diff --git a/ios/tests/test_sample.py b/ios/tests/test_sample.py new file mode 100644 index 0000000..a4d05ea --- /dev/null +++ b/ios/tests/test_sample.py @@ -0,0 +1,28 @@ +import os + +from pytest_bdd import scenario, given, when, then, parsers +from appium.webdriver.common.appiumby import AppiumBy + +FEATURES = os.path.join(os.path.dirname(os.path.dirname(__file__)), "features") + + +@scenario(os.path.join(FEATURES, "sample.feature"), "Enter text and verify the output") +def test_text_button(): + """BStackSampleApp.ipa: enter text and assert the output echoes it.""" + + +@given("I have launched the BStack sample app") +def launch_app(driver): + driver.find_element(AppiumBy.ACCESSIBILITY_ID, "Text Button").click() + + +@when(parsers.parse('I enter the text "{text}"')) +def enter_text(driver, text): + field = driver.find_element(AppiumBy.ACCESSIBILITY_ID, "Text Input") + field.send_keys(text + "\n") + + +@then(parsers.parse('the output shows "{expected}"')) +def output_matches(driver, expected): + output = driver.find_element(AppiumBy.ACCESSIBILITY_ID, "Text Output").text + assert output == expected