diff --git a/.github/workflows/sdk-sample-test.yml b/.github/workflows/sdk-sample-test.yml new file mode 100644 index 0000000..235643e --- /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 -> "unittest 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: unittest 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: unittest-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: unittest-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 python bstack_runner.py + - name: Mark status check completed + if: always() + uses: actions/github-script@v7 + env: + conclusion: ${{ job.status }} + job_name: unittest-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..c408ddf --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.vscode +__pycache__ +.pytest_cache +.venv +env +local.log +log/ +*.pyc diff --git a/README.md b/README.md index ad1e8e8..2b56c9c 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,83 @@ -# unittest-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. +# unittest + Appium with BrowserStack App Automate + +Run Python `unittest` Appium tests on real mobile devices in the BrowserStack +device cloud, instrumented automatically by the **BrowserStack SDK**. No changes +to your test logic are required — the SDK reads `browserstack.yml`, uploads/uses +the app under test, and creates one App Automate session per platform. + +This sample contains two self-contained platform directories: + +``` +unittest-appium/ +├── android/ # WikipediaSample.apk — search flow +│ ├── browserstack.yml +│ ├── bstack_runner.py +│ ├── requirements.txt +│ └── tests/bstack_sample_test.py +└── ios/ # BStackSampleApp.ipa — text-echo flow + ├── browserstack.yml + ├── bstack_runner.py + ├── requirements.txt + └── tests/bstack_sample_test.py +``` + +## Prerequisites + +- A [BrowserStack App Automate](https://app-automate.browserstack.com/) account. +- Python 3.8+. +- Your `BROWSERSTACK_USERNAME` and `BROWSERSTACK_ACCESS_KEY` + (from your [account settings](https://www.browserstack.com/accounts/settings)). + +## Setup + +```bash +git clone +cd unittest-appium-app-browserstack/android # or cd unittest-appium-app-browserstack/ios + +python3 -m venv .venv +.venv/bin/pip install -r requirements.txt +``` + +Configure credentials either in `browserstack.yml` (`userName` / `accessKey`) +or as environment variables (recommended): + +```bash +export BROWSERSTACK_USERNAME="YOUR_USERNAME" +export BROWSERSTACK_ACCESS_KEY="YOUR_ACCESS_KEY" +``` + +The app under test is referenced in `browserstack.yml` via the `app:` key as a +pre-uploaded `bs://`. To use your own build, upload it first: + +```bash +curl -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_ACCESS_KEY" \ + -X POST "https://api-cloud.browserstack.com/app-automate/upload" \ + -F "file=@WikipediaSample.apk" +# then set app: bs:// in browserstack.yml +``` + +## Run Sample Test + +From inside the platform directory: + +```bash +cd android +browserstack-sdk python bstack_runner.py +``` + +`bstack_runner.py` discovers and runs the `unittest` suite under `tests/`. The +SDK forks one process per platform in `browserstack.yml`, opening one App +Automate session per device. + +- **Android** (`WikipediaSample.apk`): taps **Search Wikipedia**, types + `BrowserStack`, and asserts that search-result `TextView`s are shown. +- **iOS** (`BStackSampleApp.ipa`): taps **Text Button**, types + `hello@browserstack.com`, and asserts the echoed **Text Output**. + +## Notes / Dashboard + +- View runs at [app-automate.browserstack.com](https://app-automate.browserstack.com/). +- With `testObservability: true`, builds also appear in + [Test Observability](https://observability.browserstack.com/). +- Device, OS, and app capabilities all come from `browserstack.yml` — the Appium + driver is created with an **empty options object**; the SDK injects the rest. 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..543555e --- /dev/null +++ b/android/browserstack.yml @@ -0,0 +1,18 @@ +userName: YOUR_USERNAME +accessKey: YOUR_ACCESS_KEY +projectName: BrowserStack Samples +buildName: appauto-unittest-appium +buildIdentifier: '#${BUILD_NUMBER}' +# `framework` lets the SDK send test context (name, status) to BrowserStack. +framework: unittest +# Pre-uploaded WikipediaSample.apk (the SDK injects this as the app under test). +app: ./WikipediaSample.apk +platforms: + - deviceName: Samsung Galaxy S22 Ultra + osVersion: "12.0" + platformName: android +parallelsPerPlatform: 1 +source: unittest:appium-sample-sdk:v1.0 +testObservability: true +debug: true +networkLogs: true diff --git a/android/bstack_runner.py b/android/bstack_runner.py new file mode 100644 index 0000000..5950e1c --- /dev/null +++ b/android/bstack_runner.py @@ -0,0 +1,13 @@ +import unittest + +# Entry point for the BrowserStack SDK generic-Python runner: +# browserstack-sdk python bstack_runner.py +# The SDK forks one process per platform listed in browserstack.yml and runs +# the discovered unittest suite in each (one BrowserStack App Automate session +# per platform). The app under test (WikipediaSample.apk) is injected from the +# `app:` key in browserstack.yml. +if __name__ == "__main__": + suite = unittest.TestLoader().discover("tests", pattern="bstack_*_test.py") + result = unittest.TextTestRunner(verbosity=2).run(suite) + if not result.wasSuccessful(): + raise SystemExit(1) diff --git a/android/requirements.txt b/android/requirements.txt new file mode 100644 index 0000000..2ca89af --- /dev/null +++ b/android/requirements.txt @@ -0,0 +1,3 @@ +Appium-Python-Client +selenium>=4.0 +browserstack-sdk @ https://sdk-assets.browserstack.com/python/browserstack_sdk-latest.tar.gz diff --git a/android/tests/bstack_sample_test.py b/android/tests/bstack_sample_test.py new file mode 100644 index 0000000..4c207b7 --- /dev/null +++ b/android/tests/bstack_sample_test.py @@ -0,0 +1,46 @@ +import unittest + +from appium import webdriver +from appium.options.android import UiAutomator2Options +from appium.webdriver.common.appiumby import AppiumBy + + +class BstackSampleTest(unittest.TestCase): + def setUp(self): + # The BrowserStack SDK injects the app + device capabilities from + # browserstack.yml at runtime, so an empty options object is enough. + options = UiAutomator2Options() + self.driver = webdriver.Remote( + "https://hub.browserstack.com/wd/hub", + options=options, + ) + self.driver.implicitly_wait(30) + + def tearDown(self): + self.driver.quit() + + def test_search_wikipedia(self): + # WikipediaSample.apk flow: + # tap the "Search Wikipedia" entry point, type a query, assert results. + search_element = self.driver.find_element( + AppiumBy.ACCESSIBILITY_ID, "Search Wikipedia" + ) + search_element.click() + + insert_text_element = self.driver.find_element( + AppiumBy.ID, "org.wikipedia.alpha:id/search_src_text" + ) + insert_text_element.send_keys("BrowserStack") + + # Allow the result list to render. + import time + time.sleep(5) + + results = self.driver.find_elements( + AppiumBy.CLASS_NAME, "android.widget.TextView" + ) + self.assertGreater(len(results), 0) + + +if __name__ == "__main__": + unittest.main() 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..7f24ddb --- /dev/null +++ b/ios/browserstack.yml @@ -0,0 +1,18 @@ +userName: YOUR_USERNAME +accessKey: YOUR_ACCESS_KEY +projectName: BrowserStack Samples +buildName: appauto-unittest-appium +buildIdentifier: '#${BUILD_NUMBER}' +# `framework` lets the SDK send test context (name, status) to BrowserStack. +framework: unittest +# Pre-uploaded BStackSampleApp.ipa (the SDK injects this as the app under test). +app: ./BStackSampleApp.ipa +platforms: + - deviceName: iPhone 14 Pro + osVersion: '16' + platformName: ios +parallelsPerPlatform: 1 +source: unittest:appium-sample-sdk:v1.0 +testObservability: true +debug: true +networkLogs: true diff --git a/ios/bstack_runner.py b/ios/bstack_runner.py new file mode 100644 index 0000000..b371ab3 --- /dev/null +++ b/ios/bstack_runner.py @@ -0,0 +1,13 @@ +import unittest + +# Entry point for the BrowserStack SDK generic-Python runner: +# browserstack-sdk python bstack_runner.py +# The SDK forks one process per platform listed in browserstack.yml and runs +# the discovered unittest suite in each (one BrowserStack App Automate session +# per platform). The app under test (BStackSampleApp.ipa) is injected from the +# `app:` key in browserstack.yml. +if __name__ == "__main__": + suite = unittest.TestLoader().discover("tests", pattern="bstack_*_test.py") + result = unittest.TextTestRunner(verbosity=2).run(suite) + if not result.wasSuccessful(): + raise SystemExit(1) diff --git a/ios/requirements.txt b/ios/requirements.txt new file mode 100644 index 0000000..2ca89af --- /dev/null +++ b/ios/requirements.txt @@ -0,0 +1,3 @@ +Appium-Python-Client +selenium>=4.0 +browserstack-sdk @ https://sdk-assets.browserstack.com/python/browserstack_sdk-latest.tar.gz diff --git a/ios/tests/bstack_sample_test.py b/ios/tests/bstack_sample_test.py new file mode 100644 index 0000000..b717127 --- /dev/null +++ b/ios/tests/bstack_sample_test.py @@ -0,0 +1,35 @@ +import unittest + +from appium import webdriver +from appium.options.ios import XCUITestOptions +from appium.webdriver.common.appiumby import AppiumBy + + +class BstackSampleTest(unittest.TestCase): + def setUp(self): + # The BrowserStack SDK injects the app + device capabilities from + # browserstack.yml at runtime, so an empty options object is enough. + options = XCUITestOptions() + self.driver = webdriver.Remote( + "https://hub.browserstack.com/wd/hub", + options=options, + ) + self.driver.implicitly_wait(30) + + def tearDown(self): + self.driver.quit() + + def test_text_echo(self): + # BStackSampleApp.ipa flow: + # open the text screen, type a value, assert it is echoed back. + self.driver.find_element(AppiumBy.ACCESSIBILITY_ID, "Text Button").click() + + text_input = self.driver.find_element(AppiumBy.ACCESSIBILITY_ID, "Text Input") + text_input.send_keys("hello@browserstack.com\n") + + output = self.driver.find_element(AppiumBy.ACCESSIBILITY_ID, "Text Output") + self.assertEqual(output.text, "hello@browserstack.com") + + +if __name__ == "__main__": + unittest.main()