-
Notifications
You must be signed in to change notification settings - Fork 1
Working with E2E Testing
The Showcase sample includes a Playwright end-to-end test suite that tests both the Blazor WASM and React frontends using the same test specs.
test/Showcase.E2ETests/
βββ fixtures/test.ts # Custom base test exposing appType fixture
βββ page-objects/*.page.ts # Thin page wrappers with role-based selectors
βββ specs/*.spec.ts # Test specs (each runs once per app)
βββ playwright.config.ts # Two projects: blazor + react
βββ package.json
βββ tsconfig.json
Key design: Playwright's multi-project feature runs every spec file against both apps automatically. page.goto('/accounts') resolves to http://localhost:5048/accounts for Blazor and http://localhost:5173/accounts for React.
Both MudBlazor and MUI render semantic HTML, so role-based selectors work across both apps:
page.getByRole('heading', { name: 'Accounts' }) // both apps
page.getByRole('button', { name: 'Create' }) // both apps
page.getByLabel(/name/i) // both apps
page.getByRole('table') // both appsOnly the navigation sidebar needs app-specific selectors (Blazor renders <a> links, React renders <div role="button">). The navigation.page.ts page object handles this via the appType fixture.
cd test/Showcase.E2ETests
npm install
npx playwright install chromiumThe webServer config in playwright.config.ts auto-starts the API and both frontends. If they're already running (e.g., via Aspire), Playwright reuses the existing servers.
npm test # run all tests (both apps)
npm run test:blazor # Blazor only
npm run test:react # React only
npm run test:ui # interactive UI mode
npm run report # view HTML report from last runnpx playwright test accounts.spec.ts # single spec file
npx playwright test --headed # visible browser
npx playwright test --headed --project=react # visible browser, React only
npx playwright test --debug # step-by-step debugger| App | Port | Source |
|---|---|---|
| Showcase.Api | 15046 | Properties/launchSettings.json |
| Showcase.BlazorApp | 5048 | Properties/launchSettings.json |
| Showcase.ReactApp | 5173 | vite.config.ts |
Each page has a thin page object wrapping its key elements. Page objects use Locator properties and action methods β no abstract base classes or inheritance hierarchies.
// page-objects/accounts.page.ts
export class AccountsPage {
readonly heading: Locator;
readonly nameInput: Locator;
readonly createButton: Locator;
constructor(page: Page) {
this.heading = page.getByRole('heading', { name: 'Accounts', exact: true });
this.nameInput = page.getByLabel(/^name$/i);
this.createButton = page.getByRole('button', { name: /^create$/i });
}
async goto() { await this.page.goto('/accounts'); }
async createAccount(name: string) { /* ... */ }
}When Blazor and React render different text for the same element, use regex:
// Blazor: "Paginated Accounts", React: "Accounts (Paginated)"
this.heading = page.getByRole('heading', { name: /paginated|accounts.*paginated/i });When only navigation differs, use the appType fixture:
import type { AppType } from '../fixtures/test';
export class NavigationPage {
constructor(private page: Page, private appType: AppType) {}
async clickNavItem(label: string) {
if (this.appType === 'blazor') {
await this.page.getByRole('link', { name: label }).click();
} else {
await this.page.getByRole('button', { name: label }).click();
}
}
}15 spec files covering all Showcase pages:
| Spec | Route | What It Tests |
|---|---|---|
| home | / |
Dashboard heading, feature cards |
| accounts | /accounts |
CRUD: create form, load, table |
| accounts-paginated | /accounts/paginated |
Page size, load, pagination |
| accounts-streaming | /accounts/async-enumerable |
Start/cancel/clear streaming |
| tasks | /tasks |
CRUD: create form, load, table |
| users | /users |
Search, filters, user list |
| user-form | /users/create |
Form fields, submit, cancel |
| user-details | /users/:id |
Profile display, buttons |
| files | /files |
File cards, preview |
| files-upload | /files/upload |
Upload tabs |
| notifications | /notifications |
Connect/disconnect controls |
| notification-subscriptions | /notifications/subscriptions |
Create form |
| webhook-demo | /webhooks/demo |
Page content |
| exception-testing | /testing/exceptions |
Run all tests, results |
| navigation | sidebar | Nav items, click navigation |
- Create a page object in
page-objects/with role-based selectors - Create a spec in
specs/importing{ test, expect }from'../fixtures/test' - Use
getByRole,getByLabel, andgetByTextβ avoid CSS selectors - Use regex when Blazor and React render different text
- Only use
appTypewhen the DOM structure differs (not just text)
Never import from
@playwright/testdirectly in spec files β always use../fixtures/testso theappTypefixture is available.
π Home
- πΌ FAQ Business Value
- π Getting Started with Basic
- π οΈ Getting Started with CLI
- π Migration Guide
- β¬οΈ Upgrading to v2
- π Working with OpenAPI
- π³οΈ Working with Nullability
- π οΈ Working with CLI
- π How-To Guides
- π Working with Security
- π¦ Working with Rate Limiting
- π Working with Resilience
- ποΈ Working with Caching
- π’ Working with Versioning
- β Working with Validations
- π Working with Webhooks
- βοΈ Working with Aspire
- π£οΈ Working with Endpoint Definitions
- π Working with Multi-Part Specs
- π§ͺ Working with Code Coverage
- π Working with C# Client
- π§ͺ Working with C# Client Testing
- π¦ Working with TypeScript Client
- πͺ Showcase Demo
- π§ͺ Working with E2E Testing
- βοΈ Working with Configuration
- π Marker Files
- π API Reference
- π Analyzer Rules
- β FAQ and Troubleshooting
- πΊοΈ Roadmap
- π§ Development Notes
- π¦ GitHub Repository
- π₯ NuGet Package
- π Report Issues