From 6a37d35f4d3d0d89b7679dae2a4d302942ae035c Mon Sep 17 00:00:00 2001 From: Jimesh Chokshi Date: Tue, 19 May 2026 11:48:41 +0530 Subject: [PATCH 1/6] Add BrowserStack SDK + Playwright NUnit sample Customer-facing starting point for running NUnit + Microsoft.Playwright .NET tests on BrowserStack via the BrowserStack .NET SDK (BrowserStack.TestAdapter). Coexists with browserstack/csharp-playwright-browserstack as the modern SDK-only variant; mirrors the shape of the xUnit + Reqnroll sample at browserstack/xunit-reqnroll-playwright-browserstack but adapted for NUnit. Layout: NunitPlaywrightBrowserstack.sln NunitPlaywrightBrowserstack.Tests/ NunitPlaywrightBrowserstack.Tests.csproj (NUnit 4.3.2 + Playwright + BS.TestAdapter) browserstack.yml (4 platforms x parallelsPerPlatform:2 = 8 sessions) AssemblyInfo.cs (NUnit fixture-level parallelism) PlaywrightFixtureBase.cs ([SetUp]/[TearDown]; SDK rewrites the launch) BStackDemoCartTest.cs (bstackdemo add-to-cart) BStackLocalSampleTest.cs (Local + bs-local.com:45454 harness) .github/workflows/sanity-workflow.yml (windows-latest + macos-latest; both filters) Source tag: nunit-playwright:sample-master:v1.0 Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/sanity-workflow.yml | 123 +++++++++++++++++ .gitignore | 10 ++ .../AssemblyInfo.cs | 5 + .../BStackDemoCartTest.cs | 23 ++++ .../BStackLocalSampleTest.cs | 23 ++++ .../NunitPlaywrightBrowserstack.Tests.csproj | 27 ++++ .../PlaywrightFixtureBase.cs | 34 +++++ .../browserstack.yml | 88 +++++++++++++ NunitPlaywrightBrowserstack.sln | 22 ++++ README.md | 124 +++++++++++++++++- 10 files changed, 478 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/sanity-workflow.yml create mode 100644 .gitignore create mode 100644 NunitPlaywrightBrowserstack.Tests/AssemblyInfo.cs create mode 100644 NunitPlaywrightBrowserstack.Tests/BStackDemoCartTest.cs create mode 100644 NunitPlaywrightBrowserstack.Tests/BStackLocalSampleTest.cs create mode 100644 NunitPlaywrightBrowserstack.Tests/NunitPlaywrightBrowserstack.Tests.csproj create mode 100644 NunitPlaywrightBrowserstack.Tests/PlaywrightFixtureBase.cs create mode 100644 NunitPlaywrightBrowserstack.Tests/browserstack.yml create mode 100644 NunitPlaywrightBrowserstack.sln diff --git a/.github/workflows/sanity-workflow.yml b/.github/workflows/sanity-workflow.yml new file mode 100644 index 0000000..19c7bda --- /dev/null +++ b/.github/workflows/sanity-workflow.yml @@ -0,0 +1,123 @@ +# Sanity workflow that verifies the NUnit + Playwright BrowserStack SDK sample +# against a full commit id, mirroring browserstack/xunit-reqnroll-playwright-browserstack. +# Two test runs: +# 1. Public bstackdemo scenario (browserstackLocal: false in yml). +# 2. BrowserStack Local scenario (yml flipped to true; a python http.server +# hosts a tiny title-matching page on port 45454, the SDK starts the tunnel, +# and the test asserts that the cloud browser sees that page through bs-local.com). + +name: NUnit Playwright SDK sanity workflow on workflow_dispatch + +on: + workflow_dispatch: + inputs: + commit_sha: + description: 'The full commit id to build' + required: true + +permissions: + contents: read + checks: write + +jobs: + sanity: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + max-parallel: 1 + matrix: + dotnet: ['8.0.x'] + os: [windows-latest, macos-latest] + name: NUnit Repo ${{ matrix.dotnet }} - ${{ matrix.os }} Sample + env: + BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME }} + BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} + + steps: + - uses: actions/checkout@v3 + with: + ref: ${{ github.event.inputs.commit_sha }} + + - uses: actions/github-script@98814c53be79b1d30f795b907e553d8679345975 + id: status-check-in-progress + env: + job_name: NUnit Repo ${{ matrix.dotnet }} - ${{ matrix.os }} Sample + commit_sha: ${{ github.event.inputs.commit_sha }} + with: + github-token: ${{ github.token }} + script: | + const result = 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((err) => ({status: err.status, response: err.response})); + console.log(`The status-check response : ${result.status} Response : ${JSON.stringify(result.response)}`) + if (result.status !== 201) { + console.log('Failed to create check run') + } + + - name: Setup dotnet + uses: actions/setup-dotnet@v3 + with: + dotnet-version: ${{ matrix.dotnet }} + + - name: Strip credential placeholders so env vars take effect + # The yml ships with literal YOUR_USERNAME / YOUR_ACCESS_KEY placeholders; + # the .NET SDK only falls back to env vars when those lines are absent. + shell: bash + working-directory: NunitPlaywrightBrowserstack.Tests + run: | + sed -i.bak '/^userName:/d; /^accessKey:/d' browserstack.yml && rm -f browserstack.yml.bak + + - name: Install dependencies + run: dotnet build + + - name: Run sample tests (public bstackdemo) + working-directory: NunitPlaywrightBrowserstack.Tests + run: dotnet test --filter "FullyQualifiedName~BStackDemoCart" + + - name: Run local tests (BrowserStack Local + python http.server harness) + shell: bash + working-directory: NunitPlaywrightBrowserstack.Tests + run: | + set -u + # 1. Stand up a tiny static page with a known . + mkdir -p "$RUNNER_TEMP/bs-local-harness" + cat > "$RUNNER_TEMP/bs-local-harness/index.html" <<'HTML' + <!doctype html> + <html><head><title>BrowserStack Local Test + OK + HTML + ( cd "$RUNNER_TEMP/bs-local-harness" && python3 -m http.server 45454 ) & + HTTP_PID=$! + trap 'kill "$HTTP_PID" 2>/dev/null || true' EXIT + sleep 2 + # 2. Flip the SDK Local toggle so the SDK starts/stops the tunnel. + sed -i.bak 's/^browserstackLocal: false/browserstackLocal: true/' browserstack.yml && rm -f browserstack.yml.bak + # 3. Run only the local scenario; cloud browser reaches the harness through bs-local.com. + dotnet test --filter "FullyQualifiedName~BStackLocalSample" + + - if: always() + uses: actions/github-script@98814c53be79b1d30f795b907e553d8679345975 + id: status-check-completed + env: + conclusion: ${{ job.status }} + job_name: NUnit Repo ${{ matrix.dotnet }} - ${{ matrix.os }} Sample + commit_sha: ${{ github.event.inputs.commit_sha }} + with: + github-token: ${{ github.token }} + script: | + const result = 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((err) => ({status: err.status, response: err.response})); + console.log(`The status-check response : ${result.status} Response : ${JSON.stringify(result.response)}`) + if (result.status !== 201) { + console.log('Failed to create check run') + } diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c555f2b --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +bin/ +obj/ +log/ +.config/ +.browserstack/ +.idea/ +.vs/ +*.user +*.suo +*.bak diff --git a/NunitPlaywrightBrowserstack.Tests/AssemblyInfo.cs b/NunitPlaywrightBrowserstack.Tests/AssemblyInfo.cs new file mode 100644 index 0000000..f87968d --- /dev/null +++ b/NunitPlaywrightBrowserstack.Tests/AssemblyInfo.cs @@ -0,0 +1,5 @@ +// Allow NUnit to run [TestFixture] classes in parallel within each SDK-spawned +// platform process. Combined with `parallelsPerPlatform: 2` in browserstack.yml, +// each `dotnet test` produces 4 platforms x 2 fixtures = 8 concurrent sessions. +[assembly: NUnit.Framework.Parallelizable(NUnit.Framework.ParallelScope.Fixtures)] +[assembly: NUnit.Framework.LevelOfParallelism(4)] diff --git a/NunitPlaywrightBrowserstack.Tests/BStackDemoCartTest.cs b/NunitPlaywrightBrowserstack.Tests/BStackDemoCartTest.cs new file mode 100644 index 0000000..37569e0 --- /dev/null +++ b/NunitPlaywrightBrowserstack.Tests/BStackDemoCartTest.cs @@ -0,0 +1,23 @@ +namespace NunitPlaywrightBrowserstack.Tests; + +[TestFixture] +[Parallelizable(ParallelScope.Self)] +public class BStackDemoCartTest : PlaywrightFixtureBase +{ + [Test] + public async Task AddTheFirstItemToCart() + { + await Page.GotoAsync("https://bstackdemo.com/"); + + var firstProduct = Page.Locator("[id=\"\\31 \"]"); + var titles = await firstProduct.Locator(".shelf-item__title").AllInnerTextsAsync(); + var productTitle = titles[0]; + await firstProduct.GetByText("Add to Cart").ClickAsync(); + + var quantity = await Page.Locator(".bag__quantity").InnerTextAsync(); + Assert.That(quantity, Is.EqualTo("1")); + + var cartTitle = await Page.Locator(".shelf-item__details").Locator(".title").InnerTextAsync(); + Assert.That(cartTitle, Is.EqualTo(productTitle)); + } +} diff --git a/NunitPlaywrightBrowserstack.Tests/BStackLocalSampleTest.cs b/NunitPlaywrightBrowserstack.Tests/BStackLocalSampleTest.cs new file mode 100644 index 0000000..7e9a4bd --- /dev/null +++ b/NunitPlaywrightBrowserstack.Tests/BStackLocalSampleTest.cs @@ -0,0 +1,23 @@ +namespace NunitPlaywrightBrowserstack.Tests; + +// Mirrors browserstack/csharp-playwright-browserstack -> SampleLocalTest.cs: +// page.GotoAsync("http://bs-local.com:45454/") + title.Contains("BrowserStack Local") +// +// Requires: +// * browserstack.yml has `browserstackLocal: true` +// * a local HTTP server is serving a page with title "BrowserStack Local Test" +// on port 45454 (the sanity workflow stands up `python -m http.server 45454` +// against a one-file index.html harness). +[TestFixture] +[Parallelizable(ParallelScope.Self)] +public class BStackLocalSampleTest : PlaywrightFixtureBase +{ + [Test] + public async Task ReachPrivateHostViaBrowserStackLocal() + { + await Page.GotoAsync("http://bs-local.com:45454/"); + + var title = await Page.TitleAsync(); + Assert.That(title, Does.Contain("BrowserStack Local")); + } +} diff --git a/NunitPlaywrightBrowserstack.Tests/NunitPlaywrightBrowserstack.Tests.csproj b/NunitPlaywrightBrowserstack.Tests/NunitPlaywrightBrowserstack.Tests.csproj new file mode 100644 index 0000000..cfed4f8 --- /dev/null +++ b/NunitPlaywrightBrowserstack.Tests/NunitPlaywrightBrowserstack.Tests.csproj @@ -0,0 +1,27 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + diff --git a/NunitPlaywrightBrowserstack.Tests/PlaywrightFixtureBase.cs b/NunitPlaywrightBrowserstack.Tests/PlaywrightFixtureBase.cs new file mode 100644 index 0000000..5b2d727 --- /dev/null +++ b/NunitPlaywrightBrowserstack.Tests/PlaywrightFixtureBase.cs @@ -0,0 +1,34 @@ +using Microsoft.Playwright; + +namespace NunitPlaywrightBrowserstack.Tests; + +// Base [SetUp]/[TearDown] for every NUnit fixture that needs an IPage. +// Customer code calls `pw.Chromium.LaunchAsync()` unconditionally; the +// BrowserStack SDK rewrites the launch at runtime to route to the per-platform +// browser configured in browserstack.yml (chrome / playwright-webkit / +// playwright-firefox / edge). No Chromium.ConnectAsync(wss_url) plumbing here. +public abstract class PlaywrightFixtureBase +{ + private IPlaywright? _pw; + private IBrowser? _browser; + private IBrowserContext? _context; + + protected IPage Page { get; private set; } = null!; + + [SetUp] + public async Task SetUp() + { + _pw = await Playwright.CreateAsync(); + _browser = await _pw.Chromium.LaunchAsync(); + _context = await _browser.NewContextAsync(); + Page = await _context.NewPageAsync(); + } + + [TearDown] + public async Task TearDown() + { + if (_context is not null) await _context.CloseAsync(); + if (_browser is not null) await _browser.CloseAsync(); + _pw?.Dispose(); + } +} diff --git a/NunitPlaywrightBrowserstack.Tests/browserstack.yml b/NunitPlaywrightBrowserstack.Tests/browserstack.yml new file mode 100644 index 0000000..773c15d --- /dev/null +++ b/NunitPlaywrightBrowserstack.Tests/browserstack.yml @@ -0,0 +1,88 @@ +# ============================= +# Set BrowserStack Credentials +# ============================= +# Add your BrowserStack userName and accessKey here or set BROWSERSTACK_USERNAME and +# BROWSERSTACK_ACCESS_KEY as env variables. +# NOTE (.NET SDK quirk): when these fields are present in the yml, the SDK uses +# their literal values -- env vars override them only if the lines below are absent. +userName: YOUR_USERNAME +accessKey: YOUR_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: BrowserStack Samples +# Set `buildName` as the name of the job / testsuite being run +buildName: nunit-playwright-browserstack +# `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} +# Set `framework` of your test suite. Example, `nunit`, `xunit`, `mstest` +# This property is needed to send test context to BrowserStack (test name, status). +framework: nunit + +source: nunit-playwright:sample-master:v1.0 + +# ======================================= +# 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) +# Customer code in PlaywrightFixtureBase.cs calls `pw.Chromium.LaunchAsync()` +# unconditionally -- the SDK transparently routes the launch to the per-platform +# browser configured here at runtime (chrome / playwright-webkit / playwright-firefox / edge). +platforms: + - os: Windows + osVersion: 11 + browserName: chrome + browserVersion: latest + - os: OS X + osVersion: Ventura + browserName: playwright-webkit + browserVersion: latest + - os: Windows + osVersion: 11 + browserName: playwright-firefox + browserVersion: latest + - os: Windows + osVersion: 11 + browserName: edge + browserVersion: latest + +# ======================= +# 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 +# +# This sample sets parallelsPerPlatform: 2 to demonstrate NUnit's fixture-level +# parallelism on top of the SDK's per-platform fan-out: 4 platforms x 2 fixtures +# (BStackDemoCart + BStackLocalSample) = 8 concurrent sessions per `dotnet test`. +parallelsPerPlatform: 2 + +# ========================================== +# 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: false # (Default false) + +# Options to be passed to BrowserStack local in-case of advanced configurations +# browserStackLocalOptions: + # 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) diff --git a/NunitPlaywrightBrowserstack.sln b/NunitPlaywrightBrowserstack.sln new file mode 100644 index 0000000..c9b70b0 --- /dev/null +++ b/NunitPlaywrightBrowserstack.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NunitPlaywrightBrowserstack.Tests", "NunitPlaywrightBrowserstack.Tests\NunitPlaywrightBrowserstack.Tests.csproj", "{EC74430F-2BA0-40B4-86DD-7D24DDAA10BD}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {EC74430F-2BA0-40B4-86DD-7D24DDAA10BD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EC74430F-2BA0-40B4-86DD-7D24DDAA10BD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EC74430F-2BA0-40B4-86DD-7D24DDAA10BD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EC74430F-2BA0-40B4-86DD-7D24DDAA10BD}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/README.md b/README.md index 2f8c3b1..7f359d5 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,124 @@ # nunit-playwright-browserstack -Sample repo for customers + +This sample shows how to run [NUnit](https://nunit.org/) + [Playwright](https://playwright.dev/dotnet) tests on BrowserStack using the [BrowserStack .NET SDK](https://www.nuget.org/packages/BrowserStack.TestAdapter). The SDK reads `browserstack.yml`, fans your tests out across the platforms listed there, starts and stops BrowserStack Local automatically, and reports test status to the BrowserStack dashboard. Your test code stays pure `Microsoft.Playwright` + NUnit -- no manual `ConnectAsync`, no caps in code. + +![BrowserStack Logo](https://d98b8t1nnulk5.cloudfront.net/production/images/layout/logo-header.png?1469004780) + +## Run Sample Build + +* Clone the repo +* Open the solution `NunitPlaywrightBrowserstack.sln` in Visual Studio (or your IDE of choice) +* Build the solution (`dotnet build`) +* Replace the `userName` and `accessKey` placeholders in `browserstack.yml` with your [BrowserStack Username and Access Key](https://www.browserstack.com/accounts/settings). Alternatively, remove those two lines from the yml and set `BROWSERSTACK_USERNAME` and `BROWSERSTACK_ACCESS_KEY` as environment variables -- the SDK falls back to env vars only when the yml fields are absent + +### Running your tests from CLI + +```sh +cd NunitPlaywrightBrowserstack.Tests +dotnet test --filter "FullyQualifiedName~BStackDemoCart" +``` + +The sample runs `BStackDemoCartTest` across all four platforms declared in `browserstack.yml` (Windows 11 / Chrome, macOS / WebKit, Windows 11 / Firefox, Windows 11 / Edge) in parallel. + +Understand how many parallel sessions you need by using our [Parallel Test Calculator](https://www.browserstack.com/automate/parallel-calculator?ref=github). + +### Testing a private host (BrowserStack Local) + +If your app lives on `localhost`, a staging host, or behind a firewall, set `browserstackLocal: true` in `browserstack.yml` and run the local fixture: + +```sh +# 1. Stand up a tiny static page locally on port 45454 (any HTTP server works) +mkdir -p bs-local-harness && cd bs-local-harness +cat > index.html <<'HTML' + +BrowserStack Local Test +OK +HTML +python3 -m http.server 45454 & + +# 2. Flip the toggle in browserstack.yml: browserstackLocal: true + +# 3. Run the local fixture +cd ../NunitPlaywrightBrowserstack.Tests +dotnet test --filter "FullyQualifiedName~BStackLocalSample" +``` + +The SDK starts and stops the BrowserStack Local tunnel for you -- no manual binary download or lifecycle management. The cloud browser reaches your local server through `http://bs-local.com:/`. + +## Integrate your test suite + +This repository uses the BrowserStack SDK to run tests on BrowserStack. To wire the SDK into your own test suite: + +* Create a `browserstack.yml` at the project root with your BrowserStack credentials and platform list (see this repo for a working template) +* Add the `BrowserStack.TestAdapter` NuGet package: + + ```sh + dotnet add package BrowserStack.TestAdapter + ``` + +* Build the project (`dotnet build`); the SDK installs the `browserstack-sdk` dotnet tool and patches the test assembly so Playwright launches are routed to BrowserStack at runtime + +## How the SDK changes things + +- **One `browserstack.yml`** declares platforms, parallelism, the Local toggle, and reporting; the SDK picks them up automatically +- **The SDK runs platforms in parallel for you** -- one NUnit run per `(platform x parallelsPerPlatform)` cell, no per-platform branching needed +- **The SDK rewrites Playwright launches** -- `PlaywrightFixtureBase.cs` calls `pw.Chromium.LaunchAsync()` and the SDK transparently redirects to the per-platform browser configured in the yml (`chrome` / `playwright-webkit` / `playwright-firefox` / `edge`). No `Chromium.ConnectAsync(wss_url)` plumbing +- **The SDK starts and stops BrowserStack Local** when `browserstackLocal: true` -- no manual tunnel lifecycle management + +## Parallelism: SDK fan-out x NUnit + +This sample stacks two layers of parallelism: + +| Layer | Configured in | Effect | +|---|---|---| +| SDK platform fan-out | `browserstack.yml` -> `platforms` + `parallelsPerPlatform` | The SDK spawns one NUnit run per `(platform x parallelsPerPlatform)` cell | +| NUnit fixture-level parallelism | `AssemblyInfo.cs` -> `[assembly: Parallelizable(ParallelScope.Fixtures)]` | Within each NUnit run, `[TestFixture]` classes execute concurrently | + +With the shipped defaults (4 platforms x `parallelsPerPlatform: 2`, 2 fixtures: `BStackDemoCartTest` + `BStackLocalSampleTest`), a single `dotnet test` produces **up to 8 concurrent BrowserStack sessions**. Tune `parallelsPerPlatform` and `LevelOfParallelism` in `AssemblyInfo.cs` to match your BrowserStack plan. + +## Repo layout + +``` +. +├── NunitPlaywrightBrowserstack.sln +└── NunitPlaywrightBrowserstack.Tests/ + ├── NunitPlaywrightBrowserstack.Tests.csproj + ├── browserstack.yml # SDK config: credentials, 4 platforms, Local toggle, reporting + ├── AssemblyInfo.cs # NUnit fixture-level parallelism + ├── PlaywrightFixtureBase.cs # [SetUp]/[TearDown] managing IPage; SDK routes the launch + ├── BStackDemoCartTest.cs # bstackdemo add-to-cart scenario + └── BStackLocalSampleTest.cs # BrowserStack Local + bs-local.com:45454 scenario +``` + +## Notes + +* You can view your test results on the [BrowserStack Automate dashboard](https://www.browserstack.com/automate) +* To test on a different set of browsers, see our [list of supported browsers and platforms](https://www.browserstack.com/list-of-browsers-and-platforms?product=automate) +* You can export the environment variables for the Username and Access Key of your BrowserStack account: + + * For Unix-like or Mac machines: + ```sh + export BROWSERSTACK_USERNAME= && + export BROWSERSTACK_ACCESS_KEY= + ``` + * For Windows Cmd: + ```cmd + set BROWSERSTACK_USERNAME= + set BROWSERSTACK_ACCESS_KEY= + ``` + * For Windows Powershell: + ```powershell + $env:BROWSERSTACK_USERNAME= + $env:BROWSERSTACK_ACCESS_KEY= + ``` + +## Further Reading + +- [NUnit](https://nunit.org/) +- [Playwright .NET](https://playwright.dev/dotnet/) +- [BrowserStack documentation for Playwright in C#](https://www.browserstack.com/docs/automate/playwright/getting-started/c-sharp) +- [BrowserStack.TestAdapter on NuGet](https://www.nuget.org/packages/BrowserStack.TestAdapter) +- [xUnit + Reqnroll reference sample](https://github.com/browserstack/xunit-reqnroll-playwright-browserstack) +- [Original C# Playwright reference sample](https://github.com/browserstack/csharp-playwright-browserstack) + +Happy Testing! From dd1530b9d9ce29f79da3eb72018bee92c1dbf0f2 Mon Sep 17 00:00:00 2001 From: Jimesh Chokshi Date: Tue, 19 May 2026 11:58:47 +0530 Subject: [PATCH 2/6] Restore Example 1/Example 2 generic comment in Parallels block Addresses the xunit-reqnroll PR #1 review feedback from @xxshubhamxx (structural parity with junit-browserstack/junit-5/browserstack.yml). Keeps the NUnit-specific 4 platforms x 2 fixtures = 8 sessions note underneath the generic Example 1/Example 2 worked-example text. Co-Authored-By: Claude Opus 4.7 (1M context) --- NunitPlaywrightBrowserstack.Tests/browserstack.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/NunitPlaywrightBrowserstack.Tests/browserstack.yml b/NunitPlaywrightBrowserstack.Tests/browserstack.yml index 773c15d..a536e35 100644 --- a/NunitPlaywrightBrowserstack.Tests/browserstack.yml +++ b/NunitPlaywrightBrowserstack.Tests/browserstack.yml @@ -60,6 +60,10 @@ platforms: # 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 +# # This sample sets parallelsPerPlatform: 2 to demonstrate NUnit's fixture-level # parallelism on top of the SDK's per-platform fan-out: 4 platforms x 2 fixtures # (BStackDemoCart + BStackLocalSample) = 8 concurrent sessions per `dotnet test`. From 97bef302d939d11b40bc9d32d4e65e0186d174e1 Mon Sep 17 00:00:00 2001 From: Jimesh Chokshi Date: Tue, 19 May 2026 12:16:45 +0530 Subject: [PATCH 3/6] Repoint comments to xunit-reqnroll as canonical reference Per the spirit of recent review feedback that all sample repos should look + work similarly, treat xunit-reqnroll-playwright-browserstack as the closest precedent for this sample (vs csharp-playwright-browserstack, the older reference). Refreshes inline comments to point at the right hooks/scenario in xunit-reqnroll. No behavioural changes. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../BStackLocalSampleTest.cs | 5 +++-- .../PlaywrightFixtureBase.cs | 12 +++++++----- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/NunitPlaywrightBrowserstack.Tests/BStackLocalSampleTest.cs b/NunitPlaywrightBrowserstack.Tests/BStackLocalSampleTest.cs index 7e9a4bd..c659999 100644 --- a/NunitPlaywrightBrowserstack.Tests/BStackLocalSampleTest.cs +++ b/NunitPlaywrightBrowserstack.Tests/BStackLocalSampleTest.cs @@ -1,11 +1,12 @@ namespace NunitPlaywrightBrowserstack.Tests; -// Mirrors browserstack/csharp-playwright-browserstack -> SampleLocalTest.cs: +// Mirrors xunit-reqnroll-playwright-browserstack's Features/LocalSample.feature +// + StepDefinitions/LocalSampleSteps.cs: // page.GotoAsync("http://bs-local.com:45454/") + title.Contains("BrowserStack Local") // // Requires: // * browserstack.yml has `browserstackLocal: true` -// * a local HTTP server is serving a page with title "BrowserStack Local Test" +// * a local HTTP server is serving a page with title containing "BrowserStack Local" // on port 45454 (the sanity workflow stands up `python -m http.server 45454` // against a one-file index.html harness). [TestFixture] diff --git a/NunitPlaywrightBrowserstack.Tests/PlaywrightFixtureBase.cs b/NunitPlaywrightBrowserstack.Tests/PlaywrightFixtureBase.cs index 5b2d727..c1be6e4 100644 --- a/NunitPlaywrightBrowserstack.Tests/PlaywrightFixtureBase.cs +++ b/NunitPlaywrightBrowserstack.Tests/PlaywrightFixtureBase.cs @@ -2,11 +2,13 @@ namespace NunitPlaywrightBrowserstack.Tests; -// Base [SetUp]/[TearDown] for every NUnit fixture that needs an IPage. -// Customer code calls `pw.Chromium.LaunchAsync()` unconditionally; the -// BrowserStack SDK rewrites the launch at runtime to route to the per-platform -// browser configured in browserstack.yml (chrome / playwright-webkit / -// playwright-firefox / edge). No Chromium.ConnectAsync(wss_url) plumbing here. +// NUnit equivalent of xunit-reqnroll-playwright-browserstack's +// Hooks/PlaywrightHooks.cs (BeforeScenario / AfterScenario) -- a per-test +// fixture that creates an IPage. Customer code calls +// `pw.Chromium.LaunchAsync()` unconditionally; the BrowserStack SDK rewrites +// the launch at runtime to route to the per-platform browser configured in +// browserstack.yml (chrome / playwright-webkit / playwright-firefox / edge). +// No Chromium.ConnectAsync(wss_url) plumbing here. public abstract class PlaywrightFixtureBase { private IPlaywright? _pw; From ce21957f87a28e2146d2ba290aa4d7993e0d400e Mon Sep 17 00:00:00 2001 From: Jimesh-browserstack Date: Tue, 19 May 2026 16:57:12 +0530 Subject: [PATCH 4/6] Address review: trim meta/cross-repo comments - BStackLocalSampleTest.cs: drop xunit-reqnroll pointer; keep a one-line `browserstackLocal` + port-45454 prerequisite note. - AssemblyInfo.cs: collapse parallelism explainer to a single line about the SDK-spawned-process invariant. Co-Authored-By: Claude Opus 4.7 (1M context) --- NunitPlaywrightBrowserstack.Tests/AssemblyInfo.cs | 4 +--- .../BStackLocalSampleTest.cs | 11 ++--------- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/NunitPlaywrightBrowserstack.Tests/AssemblyInfo.cs b/NunitPlaywrightBrowserstack.Tests/AssemblyInfo.cs index f87968d..e9f9434 100644 --- a/NunitPlaywrightBrowserstack.Tests/AssemblyInfo.cs +++ b/NunitPlaywrightBrowserstack.Tests/AssemblyInfo.cs @@ -1,5 +1,3 @@ -// Allow NUnit to run [TestFixture] classes in parallel within each SDK-spawned -// platform process. Combined with `parallelsPerPlatform: 2` in browserstack.yml, -// each `dotnet test` produces 4 platforms x 2 fixtures = 8 concurrent sessions. +// Run [TestFixture] classes in parallel within each SDK-spawned platform process. [assembly: NUnit.Framework.Parallelizable(NUnit.Framework.ParallelScope.Fixtures)] [assembly: NUnit.Framework.LevelOfParallelism(4)] diff --git a/NunitPlaywrightBrowserstack.Tests/BStackLocalSampleTest.cs b/NunitPlaywrightBrowserstack.Tests/BStackLocalSampleTest.cs index c659999..75de34d 100644 --- a/NunitPlaywrightBrowserstack.Tests/BStackLocalSampleTest.cs +++ b/NunitPlaywrightBrowserstack.Tests/BStackLocalSampleTest.cs @@ -1,14 +1,7 @@ namespace NunitPlaywrightBrowserstack.Tests; -// Mirrors xunit-reqnroll-playwright-browserstack's Features/LocalSample.feature -// + StepDefinitions/LocalSampleSteps.cs: -// page.GotoAsync("http://bs-local.com:45454/") + title.Contains("BrowserStack Local") -// -// Requires: -// * browserstack.yml has `browserstackLocal: true` -// * a local HTTP server is serving a page with title containing "BrowserStack Local" -// on port 45454 (the sanity workflow stands up `python -m http.server 45454` -// against a one-file index.html harness). +// Requires browserstack.yml has `browserstackLocal: true` and a local HTTP server +// is serving a page with title containing "BrowserStack Local" on port 45454. [TestFixture] [Parallelizable(ParallelScope.Self)] public class BStackLocalSampleTest : PlaywrightFixtureBase From ed3541a4a94661831e7a8bc784b411119d96e55a Mon Sep 17 00:00:00 2001 From: Jimesh-browserstack Date: Wed, 20 May 2026 12:38:20 +0530 Subject: [PATCH 5/6] Address review: ppp=1, local=true, Category filters Align with nunit-browserstack (Selenium) sample conventions: - browserstack.yml: parallelsPerPlatform 2 -> 1, browserstackLocal false -> true - Tag fixtures with [Category("sample-test")] / [Category("sample-local-test")] so `dotnet test --filter "Category=..."` scopes the run (matches nunit-browserstack/SampleTest.cs and SampleLocalTest.cs) - README: swap FullyQualifiedName filters for Category= filters; drop the now-redundant "flip the toggle" step; update parallelism narrative to ppp=1 (4 platforms x 1 x 2 NUnit-parallel fixtures = still 8 concurrent sessions) Co-Authored-By: Claude Opus 4.7 (1M context) --- .../BStackDemoCartTest.cs | 1 + .../BStackLocalSampleTest.cs | 1 + NunitPlaywrightBrowserstack.Tests/browserstack.yml | 6 +++--- README.md | 12 +++++------- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/NunitPlaywrightBrowserstack.Tests/BStackDemoCartTest.cs b/NunitPlaywrightBrowserstack.Tests/BStackDemoCartTest.cs index 37569e0..1d765a4 100644 --- a/NunitPlaywrightBrowserstack.Tests/BStackDemoCartTest.cs +++ b/NunitPlaywrightBrowserstack.Tests/BStackDemoCartTest.cs @@ -1,6 +1,7 @@ namespace NunitPlaywrightBrowserstack.Tests; [TestFixture] +[Category("sample-test")] [Parallelizable(ParallelScope.Self)] public class BStackDemoCartTest : PlaywrightFixtureBase { diff --git a/NunitPlaywrightBrowserstack.Tests/BStackLocalSampleTest.cs b/NunitPlaywrightBrowserstack.Tests/BStackLocalSampleTest.cs index 75de34d..0311ecd 100644 --- a/NunitPlaywrightBrowserstack.Tests/BStackLocalSampleTest.cs +++ b/NunitPlaywrightBrowserstack.Tests/BStackLocalSampleTest.cs @@ -3,6 +3,7 @@ namespace NunitPlaywrightBrowserstack.Tests; // Requires browserstack.yml has `browserstackLocal: true` and a local HTTP server // is serving a page with title containing "BrowserStack Local" on port 45454. [TestFixture] +[Category("sample-local-test")] [Parallelizable(ParallelScope.Self)] public class BStackLocalSampleTest : PlaywrightFixtureBase { diff --git a/NunitPlaywrightBrowserstack.Tests/browserstack.yml b/NunitPlaywrightBrowserstack.Tests/browserstack.yml index a536e35..f805363 100644 --- a/NunitPlaywrightBrowserstack.Tests/browserstack.yml +++ b/NunitPlaywrightBrowserstack.Tests/browserstack.yml @@ -64,10 +64,10 @@ platforms: # # 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 # -# This sample sets parallelsPerPlatform: 2 to demonstrate NUnit's fixture-level +# This sample sets parallelsPerPlatform: 1 to demonstrate NUnit's fixture-level # parallelism on top of the SDK's per-platform fan-out: 4 platforms x 2 fixtures # (BStackDemoCart + BStackLocalSample) = 8 concurrent sessions per `dotnet test`. -parallelsPerPlatform: 2 +parallelsPerPlatform: 1 # ========================================== # BrowserStack Local @@ -75,7 +75,7 @@ parallelsPerPlatform: 2 # ========================================== # 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: false # (Default false) +browserstackLocal: true # (Default false) # Options to be passed to BrowserStack local in-case of advanced configurations # browserStackLocalOptions: diff --git a/README.md b/README.md index 7f359d5..74a2d3a 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ This sample shows how to run [NUnit](https://nunit.org/) + [Playwright](https:// ```sh cd NunitPlaywrightBrowserstack.Tests -dotnet test --filter "FullyQualifiedName~BStackDemoCart" +dotnet test --filter "Category=sample-test" ``` The sample runs `BStackDemoCartTest` across all four platforms declared in `browserstack.yml` (Windows 11 / Chrome, macOS / WebKit, Windows 11 / Firefox, Windows 11 / Edge) in parallel. @@ -24,7 +24,7 @@ Understand how many parallel sessions you need by using our [Parallel Test Calcu ### Testing a private host (BrowserStack Local) -If your app lives on `localhost`, a staging host, or behind a firewall, set `browserstackLocal: true` in `browserstack.yml` and run the local fixture: +If your app lives on `localhost`, a staging host, or behind a firewall, run the local fixture (`browserstackLocal: true` is already set in `browserstack.yml`): ```sh # 1. Stand up a tiny static page locally on port 45454 (any HTTP server works) @@ -36,11 +36,9 @@ cat > index.html <<'HTML' HTML python3 -m http.server 45454 & -# 2. Flip the toggle in browserstack.yml: browserstackLocal: true - -# 3. Run the local fixture +# 2. Run the local fixture cd ../NunitPlaywrightBrowserstack.Tests -dotnet test --filter "FullyQualifiedName~BStackLocalSample" +dotnet test --filter "Category=sample-local-test" ``` The SDK starts and stops the BrowserStack Local tunnel for you -- no manual binary download or lifecycle management. The cloud browser reaches your local server through `http://bs-local.com:/`. @@ -74,7 +72,7 @@ This sample stacks two layers of parallelism: | SDK platform fan-out | `browserstack.yml` -> `platforms` + `parallelsPerPlatform` | The SDK spawns one NUnit run per `(platform x parallelsPerPlatform)` cell | | NUnit fixture-level parallelism | `AssemblyInfo.cs` -> `[assembly: Parallelizable(ParallelScope.Fixtures)]` | Within each NUnit run, `[TestFixture]` classes execute concurrently | -With the shipped defaults (4 platforms x `parallelsPerPlatform: 2`, 2 fixtures: `BStackDemoCartTest` + `BStackLocalSampleTest`), a single `dotnet test` produces **up to 8 concurrent BrowserStack sessions**. Tune `parallelsPerPlatform` and `LevelOfParallelism` in `AssemblyInfo.cs` to match your BrowserStack plan. +With the shipped defaults (4 platforms x `parallelsPerPlatform: 1`, 2 fixtures: `BStackDemoCartTest` + `BStackLocalSampleTest` running in parallel via NUnit), a single `dotnet test` produces **up to 8 concurrent BrowserStack sessions**. Tune `parallelsPerPlatform` and `LevelOfParallelism` in `AssemblyInfo.cs` to match your BrowserStack plan. ## Repo layout From cadbcfa5bdb7431c359f63928da5bd9256ad9f9c Mon Sep 17 00:00:00 2001 From: Jimesh-browserstack Date: Wed, 20 May 2026 12:53:50 +0530 Subject: [PATCH 6/6] Align sanity workflow with Category= filters and default-on Local Mirror the README/yml changes from ed3541a: - Filter strings: FullyQualifiedName~BStack* -> Category=sample-test / sample-local-test - Drop the in-workflow sed that flipped browserstackLocal false -> true; the yml ships with browserstackLocal: true now - Refresh the top-of-file scenario summary to match Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/sanity-workflow.yml | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/.github/workflows/sanity-workflow.yml b/.github/workflows/sanity-workflow.yml index 19c7bda..dec7f96 100644 --- a/.github/workflows/sanity-workflow.yml +++ b/.github/workflows/sanity-workflow.yml @@ -1,10 +1,10 @@ # Sanity workflow that verifies the NUnit + Playwright BrowserStack SDK sample # against a full commit id, mirroring browserstack/xunit-reqnroll-playwright-browserstack. -# Two test runs: -# 1. Public bstackdemo scenario (browserstackLocal: false in yml). -# 2. BrowserStack Local scenario (yml flipped to true; a python http.server +# Two test runs (yml ships with browserstackLocal: true; both runs benefit from it): +# 1. Public bstackdemo scenario (Category=sample-test). +# 2. BrowserStack Local scenario (Category=sample-local-test) — a python http.server # hosts a tiny title-matching page on port 45454, the SDK starts the tunnel, -# and the test asserts that the cloud browser sees that page through bs-local.com). +# and the test asserts that the cloud browser sees that page through bs-local.com. name: NUnit Playwright SDK sanity workflow on workflow_dispatch @@ -76,7 +76,7 @@ jobs: - name: Run sample tests (public bstackdemo) working-directory: NunitPlaywrightBrowserstack.Tests - run: dotnet test --filter "FullyQualifiedName~BStackDemoCart" + run: dotnet test --filter "Category=sample-test" - name: Run local tests (BrowserStack Local + python http.server harness) shell: bash @@ -94,10 +94,8 @@ jobs: HTTP_PID=$! trap 'kill "$HTTP_PID" 2>/dev/null || true' EXIT sleep 2 - # 2. Flip the SDK Local toggle so the SDK starts/stops the tunnel. - sed -i.bak 's/^browserstackLocal: false/browserstackLocal: true/' browserstack.yml && rm -f browserstack.yml.bak - # 3. Run only the local scenario; cloud browser reaches the harness through bs-local.com. - dotnet test --filter "FullyQualifiedName~BStackLocalSample" + # 2. Run only the local scenario; cloud browser reaches the harness through bs-local.com (browserstackLocal:true ships as default). + dotnet test --filter "Category=sample-local-test" - if: always() uses: actions/github-script@98814c53be79b1d30f795b907e553d8679345975