diff --git a/.github/workflows/sanity-workflow.yml b/.github/workflows/sanity-workflow.yml
new file mode 100644
index 0000000..dec7f96
--- /dev/null
+++ b/.github/workflows/sanity-workflow.yml
@@ -0,0 +1,121 @@
+# Sanity workflow that verifies the NUnit + Playwright BrowserStack SDK sample
+# against a full commit id, mirroring browserstack/xunit-reqnroll-playwright-browserstack.
+# 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.
+
+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 "Category=sample-test"
+
+ - 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'
+
+ 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. 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
+ 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..e9f9434
--- /dev/null
+++ b/NunitPlaywrightBrowserstack.Tests/AssemblyInfo.cs
@@ -0,0 +1,3 @@
+// 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/BStackDemoCartTest.cs b/NunitPlaywrightBrowserstack.Tests/BStackDemoCartTest.cs
new file mode 100644
index 0000000..1d765a4
--- /dev/null
+++ b/NunitPlaywrightBrowserstack.Tests/BStackDemoCartTest.cs
@@ -0,0 +1,24 @@
+namespace NunitPlaywrightBrowserstack.Tests;
+
+[TestFixture]
+[Category("sample-test")]
+[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..0311ecd
--- /dev/null
+++ b/NunitPlaywrightBrowserstack.Tests/BStackLocalSampleTest.cs
@@ -0,0 +1,18 @@
+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
+{
+ [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..c1be6e4
--- /dev/null
+++ b/NunitPlaywrightBrowserstack.Tests/PlaywrightFixtureBase.cs
@@ -0,0 +1,36 @@
+using Microsoft.Playwright;
+
+namespace NunitPlaywrightBrowserstack.Tests;
+
+// 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;
+ 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..f805363
--- /dev/null
+++ b/NunitPlaywrightBrowserstack.Tests/browserstack.yml
@@ -0,0 +1,92 @@
+# =============================
+# 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
+#
+# 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: 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: 1
+
+# ==========================================
+# BrowserStack Local
+# (For localhost, staging/private websites)
+# ==========================================
+# Set browserstackLocal to true if your website under test is not accessible publicly over the internet
+# Learn more about how BrowserStack Local works here -> https://www.browserstack.com/docs/automate/selenium/local-testing-introduction
+browserstackLocal: true # (Default false)
+
+# 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..74a2d3a 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,122 @@
# 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.
+
+
+
+## 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 "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.
+
+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, 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)
+mkdir -p bs-local-harness && cd bs-local-harness
+cat > index.html <<'HTML'
+
+BrowserStack Local Test
+OK
+HTML
+python3 -m http.server 45454 &
+
+# 2. Run the local fixture
+cd ../NunitPlaywrightBrowserstack.Tests
+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:/`.
+
+## 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: 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
+
+```
+.
+├── 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!