Skip to content

Commit 6a37d35

Browse files
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) <noreply@anthropic.com>
1 parent 5277ba2 commit 6a37d35

10 files changed

Lines changed: 478 additions & 1 deletion
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
# Sanity workflow that verifies the NUnit + Playwright BrowserStack SDK sample
2+
# against a full commit id, mirroring browserstack/xunit-reqnroll-playwright-browserstack.
3+
# Two test runs:
4+
# 1. Public bstackdemo scenario (browserstackLocal: false in yml).
5+
# 2. BrowserStack Local scenario (yml flipped to true; a python http.server
6+
# hosts a tiny title-matching page on port 45454, the SDK starts the tunnel,
7+
# and the test asserts that the cloud browser sees that page through bs-local.com).
8+
9+
name: NUnit Playwright SDK sanity workflow on workflow_dispatch
10+
11+
on:
12+
workflow_dispatch:
13+
inputs:
14+
commit_sha:
15+
description: 'The full commit id to build'
16+
required: true
17+
18+
permissions:
19+
contents: read
20+
checks: write
21+
22+
jobs:
23+
sanity:
24+
runs-on: ${{ matrix.os }}
25+
strategy:
26+
fail-fast: false
27+
max-parallel: 1
28+
matrix:
29+
dotnet: ['8.0.x']
30+
os: [windows-latest, macos-latest]
31+
name: NUnit Repo ${{ matrix.dotnet }} - ${{ matrix.os }} Sample
32+
env:
33+
BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME }}
34+
BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }}
35+
36+
steps:
37+
- uses: actions/checkout@v3
38+
with:
39+
ref: ${{ github.event.inputs.commit_sha }}
40+
41+
- uses: actions/github-script@98814c53be79b1d30f795b907e553d8679345975
42+
id: status-check-in-progress
43+
env:
44+
job_name: NUnit Repo ${{ matrix.dotnet }} - ${{ matrix.os }} Sample
45+
commit_sha: ${{ github.event.inputs.commit_sha }}
46+
with:
47+
github-token: ${{ github.token }}
48+
script: |
49+
const result = await github.rest.checks.create({
50+
owner: context.repo.owner,
51+
repo: context.repo.repo,
52+
name: process.env.job_name,
53+
head_sha: process.env.commit_sha,
54+
status: 'in_progress'
55+
}).catch((err) => ({status: err.status, response: err.response}));
56+
console.log(`The status-check response : ${result.status} Response : ${JSON.stringify(result.response)}`)
57+
if (result.status !== 201) {
58+
console.log('Failed to create check run')
59+
}
60+
61+
- name: Setup dotnet
62+
uses: actions/setup-dotnet@v3
63+
with:
64+
dotnet-version: ${{ matrix.dotnet }}
65+
66+
- name: Strip credential placeholders so env vars take effect
67+
# The yml ships with literal YOUR_USERNAME / YOUR_ACCESS_KEY placeholders;
68+
# the .NET SDK only falls back to env vars when those lines are absent.
69+
shell: bash
70+
working-directory: NunitPlaywrightBrowserstack.Tests
71+
run: |
72+
sed -i.bak '/^userName:/d; /^accessKey:/d' browserstack.yml && rm -f browserstack.yml.bak
73+
74+
- name: Install dependencies
75+
run: dotnet build
76+
77+
- name: Run sample tests (public bstackdemo)
78+
working-directory: NunitPlaywrightBrowserstack.Tests
79+
run: dotnet test --filter "FullyQualifiedName~BStackDemoCart"
80+
81+
- name: Run local tests (BrowserStack Local + python http.server harness)
82+
shell: bash
83+
working-directory: NunitPlaywrightBrowserstack.Tests
84+
run: |
85+
set -u
86+
# 1. Stand up a tiny static page with a known <title>.
87+
mkdir -p "$RUNNER_TEMP/bs-local-harness"
88+
cat > "$RUNNER_TEMP/bs-local-harness/index.html" <<'HTML'
89+
<!doctype html>
90+
<html><head><title>BrowserStack Local Test</title></head>
91+
<body>OK</body></html>
92+
HTML
93+
( cd "$RUNNER_TEMP/bs-local-harness" && python3 -m http.server 45454 ) &
94+
HTTP_PID=$!
95+
trap 'kill "$HTTP_PID" 2>/dev/null || true' EXIT
96+
sleep 2
97+
# 2. Flip the SDK Local toggle so the SDK starts/stops the tunnel.
98+
sed -i.bak 's/^browserstackLocal: false/browserstackLocal: true/' browserstack.yml && rm -f browserstack.yml.bak
99+
# 3. Run only the local scenario; cloud browser reaches the harness through bs-local.com.
100+
dotnet test --filter "FullyQualifiedName~BStackLocalSample"
101+
102+
- if: always()
103+
uses: actions/github-script@98814c53be79b1d30f795b907e553d8679345975
104+
id: status-check-completed
105+
env:
106+
conclusion: ${{ job.status }}
107+
job_name: NUnit Repo ${{ matrix.dotnet }} - ${{ matrix.os }} Sample
108+
commit_sha: ${{ github.event.inputs.commit_sha }}
109+
with:
110+
github-token: ${{ github.token }}
111+
script: |
112+
const result = await github.rest.checks.create({
113+
owner: context.repo.owner,
114+
repo: context.repo.repo,
115+
name: process.env.job_name,
116+
head_sha: process.env.commit_sha,
117+
status: 'completed',
118+
conclusion: process.env.conclusion
119+
}).catch((err) => ({status: err.status, response: err.response}));
120+
console.log(`The status-check response : ${result.status} Response : ${JSON.stringify(result.response)}`)
121+
if (result.status !== 201) {
122+
console.log('Failed to create check run')
123+
}

.gitignore

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
bin/
2+
obj/
3+
log/
4+
.config/
5+
.browserstack/
6+
.idea/
7+
.vs/
8+
*.user
9+
*.suo
10+
*.bak
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
// Allow NUnit to run [TestFixture] classes in parallel within each SDK-spawned
2+
// platform process. Combined with `parallelsPerPlatform: 2` in browserstack.yml,
3+
// each `dotnet test` produces 4 platforms x 2 fixtures = 8 concurrent sessions.
4+
[assembly: NUnit.Framework.Parallelizable(NUnit.Framework.ParallelScope.Fixtures)]
5+
[assembly: NUnit.Framework.LevelOfParallelism(4)]
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
namespace NunitPlaywrightBrowserstack.Tests;
2+
3+
[TestFixture]
4+
[Parallelizable(ParallelScope.Self)]
5+
public class BStackDemoCartTest : PlaywrightFixtureBase
6+
{
7+
[Test]
8+
public async Task AddTheFirstItemToCart()
9+
{
10+
await Page.GotoAsync("https://bstackdemo.com/");
11+
12+
var firstProduct = Page.Locator("[id=\"\\31 \"]");
13+
var titles = await firstProduct.Locator(".shelf-item__title").AllInnerTextsAsync();
14+
var productTitle = titles[0];
15+
await firstProduct.GetByText("Add to Cart").ClickAsync();
16+
17+
var quantity = await Page.Locator(".bag__quantity").InnerTextAsync();
18+
Assert.That(quantity, Is.EqualTo("1"));
19+
20+
var cartTitle = await Page.Locator(".shelf-item__details").Locator(".title").InnerTextAsync();
21+
Assert.That(cartTitle, Is.EqualTo(productTitle));
22+
}
23+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
namespace NunitPlaywrightBrowserstack.Tests;
2+
3+
// Mirrors browserstack/csharp-playwright-browserstack -> SampleLocalTest.cs:
4+
// page.GotoAsync("http://bs-local.com:45454/") + title.Contains("BrowserStack Local")
5+
//
6+
// Requires:
7+
// * browserstack.yml has `browserstackLocal: true`
8+
// * a local HTTP server is serving a page with title "BrowserStack Local Test"
9+
// on port 45454 (the sanity workflow stands up `python -m http.server 45454`
10+
// against a one-file index.html harness).
11+
[TestFixture]
12+
[Parallelizable(ParallelScope.Self)]
13+
public class BStackLocalSampleTest : PlaywrightFixtureBase
14+
{
15+
[Test]
16+
public async Task ReachPrivateHostViaBrowserStackLocal()
17+
{
18+
await Page.GotoAsync("http://bs-local.com:45454/");
19+
20+
var title = await Page.TitleAsync();
21+
Assert.That(title, Does.Contain("BrowserStack Local"));
22+
}
23+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net8.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
8+
<IsPackable>false</IsPackable>
9+
<IsTestProject>true</IsTestProject>
10+
</PropertyGroup>
11+
12+
<ItemGroup>
13+
<PackageReference Include="BrowserStack.TestAdapter" Version="0.*" />
14+
<PackageReference Include="coverlet.collector" Version="6.0.0" />
15+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
16+
<PackageReference Include="Microsoft.Playwright" Version="*" />
17+
<PackageReference Include="Microsoft.Playwright.NUnit" Version="*" />
18+
<PackageReference Include="NUnit" Version="4.3.2" />
19+
<PackageReference Include="NUnit.Analyzers" Version="4.6.0" />
20+
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
21+
</ItemGroup>
22+
23+
<ItemGroup>
24+
<Using Include="NUnit.Framework" />
25+
</ItemGroup>
26+
27+
</Project>
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
using Microsoft.Playwright;
2+
3+
namespace NunitPlaywrightBrowserstack.Tests;
4+
5+
// Base [SetUp]/[TearDown] for every NUnit fixture that needs an IPage.
6+
// Customer code calls `pw.Chromium.LaunchAsync()` unconditionally; the
7+
// BrowserStack SDK rewrites the launch at runtime to route to the per-platform
8+
// browser configured in browserstack.yml (chrome / playwright-webkit /
9+
// playwright-firefox / edge). No Chromium.ConnectAsync(wss_url) plumbing here.
10+
public abstract class PlaywrightFixtureBase
11+
{
12+
private IPlaywright? _pw;
13+
private IBrowser? _browser;
14+
private IBrowserContext? _context;
15+
16+
protected IPage Page { get; private set; } = null!;
17+
18+
[SetUp]
19+
public async Task SetUp()
20+
{
21+
_pw = await Playwright.CreateAsync();
22+
_browser = await _pw.Chromium.LaunchAsync();
23+
_context = await _browser.NewContextAsync();
24+
Page = await _context.NewPageAsync();
25+
}
26+
27+
[TearDown]
28+
public async Task TearDown()
29+
{
30+
if (_context is not null) await _context.CloseAsync();
31+
if (_browser is not null) await _browser.CloseAsync();
32+
_pw?.Dispose();
33+
}
34+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# =============================
2+
# Set BrowserStack Credentials
3+
# =============================
4+
# Add your BrowserStack userName and accessKey here or set BROWSERSTACK_USERNAME and
5+
# BROWSERSTACK_ACCESS_KEY as env variables.
6+
# NOTE (.NET SDK quirk): when these fields are present in the yml, the SDK uses
7+
# their literal values -- env vars override them only if the lines below are absent.
8+
userName: YOUR_USERNAME
9+
accessKey: YOUR_ACCESS_KEY
10+
11+
# ======================
12+
# BrowserStack Reporting
13+
# ======================
14+
# The following capabilities are used to set up reporting on BrowserStack:
15+
# Set 'projectName' to the name of your project. Example, Marketing Website
16+
projectName: BrowserStack Samples
17+
# Set `buildName` as the name of the job / testsuite being run
18+
buildName: nunit-playwright-browserstack
19+
# `buildIdentifier` is a unique id to differentiate every execution that gets appended to
20+
# buildName. Choose your buildIdentifier format from the available expressions:
21+
# ${BUILD_NUMBER} (Default): Generates an incremental counter with every execution
22+
# ${DATE_TIME}: Generates a Timestamp with every execution. Eg. 05-Nov-19:30
23+
# Read more about buildIdentifiers here -> https://www.browserstack.com/docs/automate/selenium/organize-tests
24+
buildIdentifier: '#${BUILD_NUMBER}' # Supports strings along with either/both ${expression}
25+
# Set `framework` of your test suite. Example, `nunit`, `xunit`, `mstest`
26+
# This property is needed to send test context to BrowserStack (test name, status).
27+
framework: nunit
28+
29+
source: nunit-playwright:sample-master:v1.0
30+
31+
# =======================================
32+
# Platforms (Browsers / Devices to test)
33+
# =======================================
34+
# Platforms object contains all the browser / device combinations you want to test on.
35+
# Entire list available here -> (https://www.browserstack.com/list-of-browsers-and-platforms/automate)
36+
# Customer code in PlaywrightFixtureBase.cs calls `pw.Chromium.LaunchAsync()`
37+
# unconditionally -- the SDK transparently routes the launch to the per-platform
38+
# browser configured here at runtime (chrome / playwright-webkit / playwright-firefox / edge).
39+
platforms:
40+
- os: Windows
41+
osVersion: 11
42+
browserName: chrome
43+
browserVersion: latest
44+
- os: OS X
45+
osVersion: Ventura
46+
browserName: playwright-webkit
47+
browserVersion: latest
48+
- os: Windows
49+
osVersion: 11
50+
browserName: playwright-firefox
51+
browserVersion: latest
52+
- os: Windows
53+
osVersion: 11
54+
browserName: edge
55+
browserVersion: latest
56+
57+
# =======================
58+
# Parallels per Platform
59+
# =======================
60+
# The number of parallel threads to be used for each platform set.
61+
# BrowserStack's SDK runner will select the best strategy based on the configured value
62+
#
63+
# This sample sets parallelsPerPlatform: 2 to demonstrate NUnit's fixture-level
64+
# parallelism on top of the SDK's per-platform fan-out: 4 platforms x 2 fixtures
65+
# (BStackDemoCart + BStackLocalSample) = 8 concurrent sessions per `dotnet test`.
66+
parallelsPerPlatform: 2
67+
68+
# ==========================================
69+
# BrowserStack Local
70+
# (For localhost, staging/private websites)
71+
# ==========================================
72+
# Set browserstackLocal to true if your website under test is not accessible publicly over the internet
73+
# Learn more about how BrowserStack Local works here -> https://www.browserstack.com/docs/automate/selenium/local-testing-introduction
74+
browserstackLocal: false # <boolean> (Default false)
75+
76+
# Options to be passed to BrowserStack local in-case of advanced configurations
77+
# browserStackLocalOptions:
78+
# localIdentifier: # <string> (Default: null) Needed if you need to run multiple instances of local.
79+
# forceLocal: true # <boolean> (Default: false) Set to true if you need to resolve all your traffic via BrowserStack Local tunnel.
80+
# Entire list of arguments available here -> https://www.browserstack.com/docs/automate/selenium/manage-incoming-connections
81+
82+
# ===================
83+
# Debugging features
84+
# ===================
85+
debug: false # <boolean> # Set to true if you need screenshots for every selenium command ran
86+
networkLogs: false # <boolean> Set to true to enable HAR logs capturing
87+
consoleLogs: errors # <string> Remote browser's console debug levels to be printed (Default: errors)
88+
# Available options are `disable`, `errors`, `warnings`, `info`, `verbose` (Default: errors)

NunitPlaywrightBrowserstack.sln

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
2+
Microsoft Visual Studio Solution File, Format Version 12.00
3+
# Visual Studio Version 17
4+
VisualStudioVersion = 17.0.31903.59
5+
MinimumVisualStudioVersion = 10.0.40219.1
6+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NunitPlaywrightBrowserstack.Tests", "NunitPlaywrightBrowserstack.Tests\NunitPlaywrightBrowserstack.Tests.csproj", "{EC74430F-2BA0-40B4-86DD-7D24DDAA10BD}"
7+
EndProject
8+
Global
9+
GlobalSection(SolutionConfigurationPlatforms) = preSolution
10+
Debug|Any CPU = Debug|Any CPU
11+
Release|Any CPU = Release|Any CPU
12+
EndGlobalSection
13+
GlobalSection(SolutionProperties) = preSolution
14+
HideSolutionNode = FALSE
15+
EndGlobalSection
16+
GlobalSection(ProjectConfigurationPlatforms) = postSolution
17+
{EC74430F-2BA0-40B4-86DD-7D24DDAA10BD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
18+
{EC74430F-2BA0-40B4-86DD-7D24DDAA10BD}.Debug|Any CPU.Build.0 = Debug|Any CPU
19+
{EC74430F-2BA0-40B4-86DD-7D24DDAA10BD}.Release|Any CPU.ActiveCfg = Release|Any CPU
20+
{EC74430F-2BA0-40B4-86DD-7D24DDAA10BD}.Release|Any CPU.Build.0 = Release|Any CPU
21+
EndGlobalSection
22+
EndGlobal

0 commit comments

Comments
 (0)