Stack conventions
Playwright end-to-end tests (.NET, NUnit)
End-to-end tests with Playwright for .NET (NUnit, MSTest or xUnit): role-based locators, auto-waiting assertions and parallel-safe tests.
End-to-end tests with Playwright for .NET (NUnit, MSTest or xUnit): role-based locators, auto-waiting assertions and parallel-safe tests.
Used for: playwright-dotnet.
Principles
- Test user-visible behaviour through the page, not implementation details.
- Locate elements by role and accessible name first (
Page.GetByRole(AriaRole.Button, new() { Name = "Save" })), then label, placeholder or text; test ids when nothing user-facing exists; CSS/XPath last. - Assert with web-first assertions that retry:
await Expect(locator).ToBeVisibleAsync(),ToHaveTextAsync,ToHaveURLAsync. - Never use
Thread.Sleep,Task.DelayorPage.WaitForTimeoutAsyncto wait for the UI; wait on assertions or specific responses/events. - Await every Playwright call; a missing
awaitproduces a test that passes without checking anything. - Keep tests isolated: each test gets its own browser context (the
PageTest/ContextTestbase classes do this) and its own data. - Reuse authenticated state (
StorageStatePath) instead of logging in through the UI in each test; read credentials from the environment or secret store. - Follow the repo's attributes and categories on every test (for example
[Category("...")]), because suites are selected by category. - Keep nullable reference types enabled and honour the annotations: no
!(null-forgiving) to silence warnings without a proven reason. - Be async all the way: return
Task/ValueTask, never block with.Result,.Wait()orGetAwaiter().GetResult(), and never writeasync voidexcept event handlers. - Accept and pass a
CancellationTokenthrough every I/O-bound call chain (request aborted, host shutdown). - In reusable libraries use
ConfigureAwait(false); in ASP.NET Core application code it is unnecessary (there is no synchronization context) — follow the repo's existing choice.
Conventions
Accessibility
- If an element cannot be found by role and accessible name, report the accessibility gap instead of falling back silently to CSS.
Concurrency
- Use unique test data per test so parallel workers do not collide; no static mutable state shared between tests.
- DbContext is not thread-safe: do not run parallel queries on one instance.
- Protect shared state with immutable data,
Concurrent*collections orSemaphoreSlim(async); neverlockaroundawait. - Honour cancellation tokens and avoid fire-and-forget tasks; use a hosted service or queue.
Dependencies
- Keep
Microsoft.Playwrightand the installed browsers in step (run the generatedplaywright.ps1 installafter upgrades). - Prefer framework packages (
Microsoft.Extensions.*) before third-party libraries; keep package versions consistent across projects. - Keep NuGet audit warnings visible; update vulnerable packages rather than suppressing warnings.
Documentation
- Reference the story or ticket the test verifies in a
[Description], category or comment, following the repo. - Comment why a test is serial, skipped or uses an unusual wait; the test name says what it checks.
- XML doc comments (
///) on public APIs of libraries, including exceptions thrown; keep OpenAPI metadata (WithSummary,Produces) accurate for endpoints. - Comment why, not what; keep the README's run, configuration and migration steps current when they change.
Errors
- Let assertion failures surface; do not wrap Playwright calls in try/catch that swallows exceptions.
- Give
Expect(...)assertions a timeout only where the default is wrong for a known slow step, and say why. - Throw specific exceptions (
ArgumentNullException.ThrowIfNull,ArgumentOutOfRangeException.ThrowIfNegative); never throwExceptionor catch it without rethrowing or logging. - Do not use exceptions for control flow on expected outcomes; return results or
nullwith nullable annotations. - Rethrow with
throw;, notthrow ex;, to keep the stack trace.
Logging
- Capture traces on failure (
Context.Tracing.StartAsync/StopAsyncwith a path when the test failed) rather than writing console output; useTestContext.WriteLinesparingly. - Attach the trace, screenshot and page URL to the test result on failure so the report alone explains it.
- Use structured message templates (
logger.LogInformation("Order {OrderId} shipped", id)) with stable property names. - Use log scopes for correlation; never log secrets, tokens or personal data.
- Pick levels deliberately: Information for business events, Warning for recoverable issues, Error with the exception object.
Naming
- Test classes end in
Testsand are named after the feature; test methods state the behaviour (SavesDraftInvoiceorSave_WithValidData_ShowsConfirmation). - Page objects end in
PageorComponent. - PascalCase for types, methods, properties, constants and public fields; camelCase for locals and parameters;
_camelCasefor private fields; interfaces start withI. - Async methods end with
Async; boolean members read as predicates (IsActive,HasItems). - Namespaces follow the folder structure and use file-scoped declarations.
Performance
- Reuse authenticated state and seed data via API; avoid navigation that is not part of the behaviour.
- Run independent tests in parallel and keep each test's setup to the API, not the UI.
- Project to DTOs with
Selectrather than loading full entity graphs; page large results. - Avoid synchronous I/O and
Task.Runin request paths; useIAsyncEnumerable<T>for streaming. - Use
HybridCache/IMemoryCache/output caching deliberately with explicit expiry, where the repo has a caching approach.
Security
- Never commit credentials, tokens or saved storage-state files; point tests at test/sandbox environments only.
- Read credentials from environment variables or the secret store, and validate that the target URL is a test environment before the suite runs.
- Do not log or attach pages that show secrets; mask them in screenshots (
Maskoption). - Never build selectors or JavaScript for
EvaluateAsyncfrom external input (script injection). - Keep
Microsoft.Playwrightand test packages on supported versions and act on NuGet audit warnings. - Protect endpoints with authorisation policies (
RequireAuthorization,[Authorize(Policy=...)]) and default to deny for new endpoints. - Never concatenate input into SQL: use LINQ, parameters, or
FromSql/ExecuteSqlwith interpolation (parameterised), neverFromSqlRawwith concatenated input. - Keep secrets out of
appsettings*.json; use user secrets locally and a secret store (e.g. Key Vault) in deployed environments.
Structure
- Put tests in the configured test project; page objects and helpers in their own folders within it.
- Keep environment URLs, credentials and browser options in run settings or environment variables, never in test code.
- One public type per file named after the type; keep
Program.csto composition (service registration, middleware, endpoint mapping). - Separate API, application/domain and infrastructure concerns along the lines the solution already uses; do not introduce a new layering scheme in one change.
- Use
Directory.Build.propsand central package management (Directory.Packages.props) when the repo has them.
Testing
- One behaviour per test; tests must pass in any order and in parallel (
[Parallelizable(ParallelScope.Self)]) where the suite enables it. - Use
[Ignore("reason + ticket")]instead of commenting out tests. - Every new test carries the categories the suite's filters rely on.
- Locate elements with
GetByRole,GetByLabelandGetByText, then test ids; never by CSS structure or XPath. - Never use
WaitForTimeoutAsync,Thread.SleeporTask.Delayto wait; use auto-waiting locators andExpect(locator).ToBeVisibleAsync(). - End-to-end tests cover user journeys across the integrated system; unit-level checks stay in the application's own unit tests.
- Done means tested: each new or changed behaviour in the acceptance criteria has a test that fails without it.
- Use the repo's framework (xUnit, NUnit or MSTest); name tests
Method_Scenario_ExpectedResultor the repo's convention.
Review checklist
- Are locators role/label/text based before test ids and CSS?
- Are assertions web-first
Expect(...)calls rather than one-shot boolean checks? - Is every Playwright call awaited?
- Is the test free of Thread.Sleep, Task.Delay and WaitForTimeoutAsync?
- Does every new test carry the required category attribute?
- Is the test independent of other tests and of execution order?
- Are credentials and URLs read from configuration, not hard-coded?
- Is authentication reused through storage state where the suite supports it?
- Do page objects hold actions/locators only, with assertions in tests?
- Does the test name state the behaviour it proves?
- Are nullable warnings addressed without unjustified
!operators? - Is the code async end to end with no
.Result,.Wait()orasync void(except event handlers)?
Quality checks (run when your repository has the tool configured)
| Check | Kind | Tool | Blocks the pull request |
|---|---|---|---|
playwright-dotnet-no-fixed-waits |
lint | grep | no |
dotnet-format |
format | dotnet format | no |
dotnet-build-warnaserror |
static | dotnet build (analyzers) | no |
nuget-vulnerable |
security | dotnet list package | no |
gitleaks |
security | gitleaks | no |