A small but production-shaped UI test-automation framework in C# / .NET 10 with Playwright and MSTest. It shows how I structure a maintainable suite — page objects, component objects, isolated fixtures, layered configuration, and proper secret handling — rather than a flat pile of tests.
System under test: SauceDemo, a stable public demo shop chosen deliberately so the suite stays reliable and CI-green.
Most of my automation work lives in private company repositories. This is a sanitized, self-contained showcase of the patterns I use day to day — the kind of framework a team extends, not a script someone runs once.
- .NET 10 / C#
- Playwright for browser automation
- MSTest (classic VSTest runner) with
Microsoft.Playwright.MSTestfixtures - Microsoft.Extensions.Configuration for layered configuration
- Chromium, auto-installed on first run
Prerequisite: the .NET 10 SDK.
git clone https://github.com/SnoxSoft/playwright-dotnet-framework.git
cd playwright-dotnet-framework
# Provide the (public) SauceDemo password locally — this file is git-ignored:
cp PlaywrightFramework.Tests/appsettings.local.json.example PlaywrightFramework.Tests/appsettings.local.json
# then set "Password": "secret_sauce" in that file
dotnet testBrowser binaries install themselves on the first run (see Fixtures/GlobalSetup.cs), so there is no separate playwright install step.
PlaywrightFramework.Tests/
├─ Config/ # layered configuration bound to a strongly-typed TestSettings
├─ Fixtures/ # BaseTest (isolated browser context) + GlobalSetup (browser install)
├─ Pages/ # page objects — one class per screen (LoginPage, InventoryPage)
├─ Components/ # component objects — reusable UI pieces (InventoryItem)
├─ Tests/ # the scenarios (LoginTests, CartTests)
├─ appsettings.json # non-secret config (committed)
└─ appsettings.local.json.example # template for the git-ignored secret file
Page objects vs. component objects. A page object models a whole screen and exposes intent (LoginPage.LoginAsync(user, pass)). A component object models a reusable slice of UI scoped to a root locator (InventoryItem), so one class works for any product card. Reach for a component when a widget repeats or spans pages — but don't wrap every single element in one; that trades clarity for ceremony.
Isolated, parallel tests. Every test class extends BaseTest : PageTest, which hands each test its own browser context. No shared state means tests run in parallel ([assembly: Parallelize]) without bleeding into each other.
Configuration & secrets. TestConfig builds a layered configuration and binds it to TestSettings, in increasing precedence:
appsettings.json → appsettings.{TEST_ENV}.json → appsettings.local.json → User Secrets → environment variables
Non-secret defaults are committed; the password lives only in the git-ignored appsettings.local.json, in User Secrets, or in a CI env var (TestSettings__Password). SauceDemo's credentials are public, so they're used openly here — but the mechanism is the real one. A missing value fails fast with an actionable message (TestSettings.Validate()).
Locator strategy. Prefer stable, test-dedicated hooks ([data-test=...]) and user-facing locators (roles, text) over brittle CSS/XPath, so tests survive styling changes.
Tooling choices.
- MSTest 3.x (classic VSTest runner). The .NET 10 template defaults to the new Microsoft.Testing.Platform runner, which
dotnet testand IDE Test Explorers don't discover; pinning 3.x restores the standard path and matches common enterprise setups. - Self-installing browsers.
GlobalSetuprunsplaywright installin-process, so the suite is self-sufficient in any runner or CI — no separate setup step. - Stable demo target. A flaky target undermines a green badge, so the system under test is intentionally boring and reliable.
dotnet test # all tests, headless
dotnet test --filter "FullyQualifiedName~Cart" # only the cart testsSet PWDEBUG=1 to run headed and open the Playwright Inspector for step-through debugging. In Rider, open the solution and use the Unit Tests window.
- The target is a public demo site, so the scenarios are illustrative — the patterns are the point, not exhaustive coverage of a real product.
- Cross-browser (Firefox/WebKit) isn't wired up yet; the design supports it (add browsers in
GlobalSetupand a.runsettings), it's simply out of scope for this showcase.