From f68da527ec8059c4a7f7203cf2de3e9f9f01df78 Mon Sep 17 00:00:00 2001 From: Aakash Hotchandani Date: Wed, 10 Jun 2026 18:37:43 +0530 Subject: [PATCH 1/3] Add reqnroll-nunit-appium App Automate sample (Android) --- .github/workflows/Semgrep.yml | 49 ------------ .gitignore | 21 +++++ .npmrc | 7 -- CODEOWNERS | 1 - README.md | 76 ++++++++++++++++++- android/.config/dotnet-tools.json | 5 ++ android/Features/SampleLocalTest.feature | 5 ++ android/Features/SampleTest.feature | 6 ++ android/StepDefinitions/AppiumHooks.cs | 40 ++++++++++ .../StepDefinitions/SampleLocalTestSteps.cs | 50 ++++++++++++ android/StepDefinitions/SampleTestSteps.cs | 50 ++++++++++++ android/android.csproj | 29 +++++++ android/browserstack.yml | 48 ++++++++++++ 13 files changed, 328 insertions(+), 59 deletions(-) delete mode 100644 .github/workflows/Semgrep.yml create mode 100644 .gitignore delete mode 100644 .npmrc delete mode 100644 CODEOWNERS create mode 100644 android/.config/dotnet-tools.json create mode 100644 android/Features/SampleLocalTest.feature create mode 100644 android/Features/SampleTest.feature create mode 100644 android/StepDefinitions/AppiumHooks.cs create mode 100644 android/StepDefinitions/SampleLocalTestSteps.cs create mode 100644 android/StepDefinitions/SampleTestSteps.cs create mode 100644 android/android.csproj create mode 100644 android/browserstack.yml diff --git a/.github/workflows/Semgrep.yml b/.github/workflows/Semgrep.yml deleted file mode 100644 index 5398af9..0000000 --- a/.github/workflows/Semgrep.yml +++ /dev/null @@ -1,49 +0,0 @@ -# Name of this GitHub Actions workflow. -name: Semgrep - -on: - # Scan changed files in PRs (diff-aware scanning): - # The branches below must be a subset of the branches above - pull_request: - branches: ["master", "main"] - push: - branches: ["master", "main"] - schedule: - - cron: '0 6 * * *' - - -permissions: - contents: read - -jobs: - semgrep: - # User definable name of this GitHub Actions job. - permissions: - contents: read # for actions/checkout to fetch code - security-events: write # for github/codeql-action/upload-sarif to upload SARIF results - name: semgrep/ci - # If you are self-hosting, change the following `runs-on` value: - runs-on: ubuntu-latest - - container: - # A Docker image with Semgrep installed. Do not change this. - image: returntocorp/semgrep - - # Skip any PR created by dependabot to avoid permission issues: - if: (github.actor != 'dependabot[bot]') - - steps: - # Fetch project source with GitHub Actions Checkout. - - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 - # Run the "semgrep ci" command on the command line of the docker image. - - run: semgrep ci --sarif --output=semgrep.sarif - env: - # Add the rules that Semgrep uses by setting the SEMGREP_RULES environment variable. - SEMGREP_RULES: p/default # more at semgrep.dev/explore - - - name: Upload SARIF file for GitHub Advanced Security Dashboard - uses: github/codeql-action/upload-sarif@6c089f53dd51dc3fc7e599c3cb5356453a52ca9e # v2.20.0 - with: - sarif_file: semgrep.sarif - if: always() - diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7bdb28d --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +# .NET build output +bin/ +obj/ +*.user + +# Reqnroll / test artifacts +TestResults/ +*.feature.cs + +# BrowserStack SDK logs +log/ +local.log +*.log + +# IDE +.vs/ +.vscode/ +.idea/ + +# Credentials (never commit) +.bstack.env diff --git a/.npmrc b/.npmrc deleted file mode 100644 index 33911e3..0000000 --- a/.npmrc +++ /dev/null @@ -1,7 +0,0 @@ -ignore-scripts=true -strict-ssl=true -save-exact=true -engine-strict=true -legacy-peer-deps=false -audit-level=high -access=public diff --git a/CODEOWNERS b/CODEOWNERS deleted file mode 100644 index c4a6041..0000000 --- a/CODEOWNERS +++ /dev/null @@ -1 +0,0 @@ -* @browserstack/automate-public-repos \ No newline at end of file diff --git a/README.md b/README.md index ca2fc14..5bec479 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,74 @@ -# reqnroll-nunit-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. +# Reqnroll (NUnit) + Appium with BrowserStack App Automate + +Run native Android app tests on real devices on [BrowserStack App Automate](https://app-automate.browserstack.com/) +using [Reqnroll](https://reqnroll.net/) (BDD) on top of NUnit + the Appium .NET client, integrated through the +[BrowserStack C# SDK](https://www.nuget.org/packages/BrowserStack.TestAdapter) (`BrowserStack.TestAdapter`). + +The SDK reads `browserstack.yml`, uploads/uses the app, provisions the device, and routes the Appium session +to the BrowserStack cloud — no changes to your test logic are required. + +## Prerequisites + +- A [BrowserStack account](https://www.browserstack.com/users/sign_up) (username + access key). +- [.NET SDK](https://dotnet.microsoft.com/download) 8.0 (net6.0+ is supported). +- The sample app is the public **WikipediaSample.apk**, pre-uploaded to BrowserStack and referenced in + `android/browserstack.yml` as a `bs://` URL. To use your own build, upload it via the + [App Automate upload API](https://www.browserstack.com/app-automate/rest-api) and replace the `app:` value. + +## Setup + +```bash +git clone +cd reqnroll-nunit-appium + +# Configure credentials (either edit browserstack.yml or export env vars) +export BROWSERSTACK_USERNAME="YOUR_USERNAME" +export BROWSERSTACK_ACCESS_KEY="YOUR_ACCESS_KEY" +``` + +Credentials can be set in `android/browserstack.yml` (`userName` / `accessKey`) or via the +`BROWSERSTACK_USERNAME` / `BROWSERSTACK_ACCESS_KEY` environment variables (env vars take precedence). + +This is an App Automate (mobile) sample, so the Android platform lives in its own self-contained directory +(`android/`) with its own `browserstack.yml`. + +## Run Sample Test + +The sample test drives **WikipediaSample.apk**: it taps "Search Wikipedia", types "BrowserStack", and asserts +that search results are listed. + +```bash +cd android +dotnet restore +dotnet test --filter "TestCategory=sample-test" +``` + +To run every scenario in the project: + +```bash +cd android +dotnet test +``` + +## Run Local Test + +The local test drives **LocalSample.apk** to prove the BrowserStack Local tunnel is connected. It is tagged +`@sample-local-test` and is skipped by default in the sample run above. To run it, point `app:` in +`android/browserstack.yml` at a pre-uploaded `LocalSample.apk`, set `browserstackLocal: true`, then: + +```bash +cd android +dotnet test --filter "TestCategory=sample-local-test" +``` + +The BrowserStack SDK starts and manages the Local tunnel automatically when `browserstackLocal: true` is set +in `browserstack.yml`. + +## Notes / Dashboard + +- View runs, video, device logs, and network logs on the + [App Automate dashboard](https://app-automate.browserstack.com/). +- `testObservability: true` also surfaces the build on + [Test Observability](https://observability.browserstack.com/). +- The driver is created with an empty `AppiumOptions` object; all capabilities (app, device, `bstack:options`) + are injected by the SDK from `browserstack.yml`. diff --git a/android/.config/dotnet-tools.json b/android/.config/dotnet-tools.json new file mode 100644 index 0000000..c967c89 --- /dev/null +++ b/android/.config/dotnet-tools.json @@ -0,0 +1,5 @@ +{ + "version": 1, + "isRoot": true, + "tools": {} +} diff --git a/android/Features/SampleLocalTest.feature b/android/Features/SampleLocalTest.feature new file mode 100644 index 0000000..b41403f --- /dev/null +++ b/android/Features/SampleLocalTest.feature @@ -0,0 +1,5 @@ +@sample-local-test +Feature: BrowserStack Local Testing (Local Sample App) + Scenario: Verify the BrowserStack Local tunnel from the device + Given I start the test on the Local Sample App + Then I should see the connection is up and running diff --git a/android/Features/SampleTest.feature b/android/Features/SampleTest.feature new file mode 100644 index 0000000..848bbc1 --- /dev/null +++ b/android/Features/SampleTest.feature @@ -0,0 +1,6 @@ +@sample-test +Feature: BrowserStack Sample (Wikipedia App) + Scenario: Search Wikipedia for BrowserStack + Given I try to search using the Wikipedia App + When I search with the keyword BrowserStack + Then the search results should be listed diff --git a/android/StepDefinitions/AppiumHooks.cs b/android/StepDefinitions/AppiumHooks.cs new file mode 100644 index 0000000..2599c21 --- /dev/null +++ b/android/StepDefinitions/AppiumHooks.cs @@ -0,0 +1,40 @@ +using System; +using System.Threading; +using Reqnroll; +using OpenQA.Selenium.Appium; +using OpenQA.Selenium.Appium.Android; + +namespace ReqnrollAppiumBrowserStack +{ + // Manages the Appium Android driver lifecycle for each scenario. + // + // The driver is created with an EMPTY AppiumOptions object against the + // BrowserStack hub. The BrowserStack SDK (BrowserStack.TestAdapter) + // intercepts the Appium driver construction and injects the app, device, + // and bstack:options capabilities from android/browserstack.yml — no + // capabilities are hardcoded here. + [Binding] + public class AppiumHooks + { + // Shared with the step definitions for the current scenario. + public static ThreadLocal> ThreadLocalDriver = + new ThreadLocal>(); + + [BeforeScenario(Order = 0)] + public static void Initialize() + { + AppiumOptions appiumOptions = new AppiumOptions(); + ThreadLocalDriver.Value = new AndroidDriver( + new Uri("https://hub-cloud.browserstack.com/wd/hub/"), appiumOptions); + } + + [AfterScenario] + public static void TearDown() + { + if (ThreadLocalDriver.IsValueCreated) + { + ThreadLocalDriver.Value?.Quit(); + } + } + } +} diff --git a/android/StepDefinitions/SampleLocalTestSteps.cs b/android/StepDefinitions/SampleLocalTestSteps.cs new file mode 100644 index 0000000..180e1c6 --- /dev/null +++ b/android/StepDefinitions/SampleLocalTestSteps.cs @@ -0,0 +1,50 @@ +using System; +using System.Threading; +using System.Collections.ObjectModel; +using Reqnroll; +using NUnit.Framework; +using OpenQA.Selenium; +using OpenQA.Selenium.Appium.Android; +using OpenQA.Selenium.Support.UI; + +namespace ReqnrollAppiumBrowserStack +{ + [Binding] + public class SampleLocalTestSteps + { + private readonly AndroidDriver _driver; + + public SampleLocalTestSteps() + { + _driver = AppiumHooks.ThreadLocalDriver.Value!; + } + + [Given(@"I start the test on the Local Sample App")] + public void GivenIStartTestOnLocalApp() + { + AndroidElement testAction = (AndroidElement)new WebDriverWait(_driver, TimeSpan.FromSeconds(30)) + .Until(ExpectedConditions.ElementToBeClickable( + By.Id("com.example.android.basicnetworking:id/test_action"))); + testAction.Click(); + } + + [Then(@"I should see the connection is up and running")] + public void ThenIShouldSeeUpAndRunning() + { + Thread.Sleep(5000); + ReadOnlyCollection textViews = + _driver.FindElements(By.ClassName("android.widget.TextView")); + + bool found = false; + foreach (AndroidElement element in textViews) + { + if (element.Text != null && element.Text.Contains("The active connection is")) + { + found = true; + break; + } + } + Assert.That(found, Is.True, "Expected the Local Sample App to report an active connection"); + } + } +} diff --git a/android/StepDefinitions/SampleTestSteps.cs b/android/StepDefinitions/SampleTestSteps.cs new file mode 100644 index 0000000..00bd693 --- /dev/null +++ b/android/StepDefinitions/SampleTestSteps.cs @@ -0,0 +1,50 @@ +using System; +using System.Threading; +using System.Collections.ObjectModel; +using Reqnroll; +using NUnit.Framework; +using OpenQA.Selenium; +using OpenQA.Selenium.Appium; +using OpenQA.Selenium.Appium.Android; +using OpenQA.Selenium.Support.UI; + +namespace ReqnrollAppiumBrowserStack +{ + [Binding] + public class SampleTestSteps + { + private readonly AndroidDriver _driver; + + public SampleTestSteps() + { + _driver = AppiumHooks.ThreadLocalDriver.Value!; + } + + [Given(@"I try to search using the Wikipedia App")] + public void GivenITrySearchWikipediaApp() + { + AndroidElement searchElement = (AndroidElement)new WebDriverWait(_driver, TimeSpan.FromSeconds(30)) + .Until(ExpectedConditions.ElementToBeClickable( + MobileBy.AccessibilityId("Search Wikipedia"))); + searchElement.Click(); + } + + [When(@"I search with the keyword BrowserStack")] + public void WhenISearchKeywordBrowserStack() + { + AndroidElement insertTextElement = (AndroidElement)new WebDriverWait(_driver, TimeSpan.FromSeconds(30)) + .Until(ExpectedConditions.ElementToBeClickable( + By.Id("org.wikipedia.alpha:id/search_src_text"))); + insertTextElement.SendKeys("BrowserStack"); + Thread.Sleep(5000); + } + + [Then(@"the search results should be listed")] + public void ThenSearchResultsShouldBeListed() + { + ReadOnlyCollection results = + _driver.FindElements(By.ClassName("android.widget.TextView")); + Assert.That(results.Count, Is.GreaterThan(0)); + } + } +} diff --git a/android/android.csproj b/android/android.csproj new file mode 100644 index 0000000..9c8bdc0 --- /dev/null +++ b/android/android.csproj @@ -0,0 +1,29 @@ + + + + net8.0 + ReqnrollAppiumBrowserStack + ReqnrollAppiumBrowserStack + false + enable + enable + false + + + + + + + + + + + + + + + + + + diff --git a/android/browserstack.yml b/android/browserstack.yml new file mode 100644 index 0000000..ae54c14 --- /dev/null +++ b/android/browserstack.yml @@ -0,0 +1,48 @@ +# ============================= +# 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-reqnroll-nunit-appium +buildIdentifier: '#${BUILD_NUMBER}' +# `framework` lets the SDK send test context (name, status) to BrowserStack. +framework: reqnroll + +# ========================================== +# Application under test +# ========================================== +# Pre-uploaded WikipediaSample.apk on BrowserStack (use this bs:// url verbatim). +app: bs://92d48b416632f2b1734259565ceab61b05ad0b24 + +# ======================================= +# Platforms (Devices to test) +# ======================================= +platforms: + - deviceName: Samsung Galaxy S22 Ultra + osVersion: "12.0" + platformName: android + +# ======================= +# Parallels per Platform +# ======================= +parallelsPerPlatform: 1 + +source: reqnroll-nunit-appium-app-browserstack:sample-sdk:v1.0 + +# ====================== +# Test Observability +# ====================== +testObservability: true + +# =================== +# Debugging features +# =================== +debug: true +networkLogs: true From 4a172be61eba9dd96f4182e991217e26c9ed0fcd Mon Sep 17 00:00:00 2001 From: Aakash Hotchandani Date: Thu, 11 Jun 2026 14:17:07 +0530 Subject: [PATCH 2/3] Restore scaffold files dropped by import (Semgrep CI workflow, CODEOWNERS, .npmrc) --- .github/workflows/Semgrep.yml | 49 +++++++++++++++++++++++++++++++++++ .npmrc | 7 +++++ CODEOWNERS | 1 + 3 files changed, 57 insertions(+) create mode 100644 .github/workflows/Semgrep.yml create mode 100644 .npmrc create mode 100644 CODEOWNERS diff --git a/.github/workflows/Semgrep.yml b/.github/workflows/Semgrep.yml new file mode 100644 index 0000000..5398af9 --- /dev/null +++ b/.github/workflows/Semgrep.yml @@ -0,0 +1,49 @@ +# Name of this GitHub Actions workflow. +name: Semgrep + +on: + # Scan changed files in PRs (diff-aware scanning): + # The branches below must be a subset of the branches above + pull_request: + branches: ["master", "main"] + push: + branches: ["master", "main"] + schedule: + - cron: '0 6 * * *' + + +permissions: + contents: read + +jobs: + semgrep: + # User definable name of this GitHub Actions job. + permissions: + contents: read # for actions/checkout to fetch code + security-events: write # for github/codeql-action/upload-sarif to upload SARIF results + name: semgrep/ci + # If you are self-hosting, change the following `runs-on` value: + runs-on: ubuntu-latest + + container: + # A Docker image with Semgrep installed. Do not change this. + image: returntocorp/semgrep + + # Skip any PR created by dependabot to avoid permission issues: + if: (github.actor != 'dependabot[bot]') + + steps: + # Fetch project source with GitHub Actions Checkout. + - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + # Run the "semgrep ci" command on the command line of the docker image. + - run: semgrep ci --sarif --output=semgrep.sarif + env: + # Add the rules that Semgrep uses by setting the SEMGREP_RULES environment variable. + SEMGREP_RULES: p/default # more at semgrep.dev/explore + + - name: Upload SARIF file for GitHub Advanced Security Dashboard + uses: github/codeql-action/upload-sarif@6c089f53dd51dc3fc7e599c3cb5356453a52ca9e # v2.20.0 + with: + sarif_file: semgrep.sarif + if: always() + diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..33911e3 --- /dev/null +++ b/.npmrc @@ -0,0 +1,7 @@ +ignore-scripts=true +strict-ssl=true +save-exact=true +engine-strict=true +legacy-peer-deps=false +audit-level=high +access=public diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000..c4a6041 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1 @@ +* @browserstack/automate-public-repos \ No newline at end of file From c32f8da80422dae5db052889d76e733eced3a2bf Mon Sep 17 00:00:00 2001 From: Aakash Hotchandani Date: Fri, 12 Jun 2026 11:38:56 +0530 Subject: [PATCH 3/3] Add BrowserStack SDK sample-test GitHub Actions workflow (workflow_dispatch) --- .github/workflows/sdk-sample-test.yml | 72 +++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 .github/workflows/sdk-sample-test.yml diff --git a/.github/workflows/sdk-sample-test.yml b/.github/workflows/sdk-sample-test.yml new file mode 100644 index 0000000..970f40c --- /dev/null +++ b/.github/workflows/sdk-sample-test.yml @@ -0,0 +1,72 @@ +# Runs the BrowserStack SDK sample against a given commit and reports a status check. +# Trigger: Actions tab -> "Reqnroll (NUnit) 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: Reqnroll (NUnit) Appium App Automate SDK sample test + +on: + workflow_dispatch: + inputs: + commit_sha: + description: 'The full commit id to build' + required: true + +jobs: + sdk-sample: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + max-parallel: 3 + matrix: + os: [windows-latest] + dotnet: ['8.0.x'] + name: reqnroll-nunit-appium .NET ${{ matrix.dotnet }} 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: reqnroll-nunit-appium .NET ${{ matrix.dotnet }} 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-dotnet@v4 + with: + dotnet-version: ${{ matrix.dotnet }} + - name: Restore + run: | + dotnet tool restore + dotnet restore + - name: Run sample test + run: | + dotnet test --filter "TestCategory=sample-test" + - name: Mark status check completed + if: always() + uses: actions/github-script@v7 + env: + conclusion: ${{ job.status }} + job_name: reqnroll-nunit-appium .NET ${{ matrix.dotnet }} 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));