Three test projects under tests/, two suites:
| Project | Suite | What it is |
|---|---|---|
IgniteUI.Blazor.Tests |
Unit | xUnit + bUnit, multi-targeted net8.0/net9.0/net10.0; renders components in-process with a recording JS runtime — no browser |
IgniteUI.Blazor.Lite.IntegrationTests |
Integration | NUnit + Playwright; one generated test fixture per component |
IgniteUI.Blazor.Lite.TestBed |
(integration host) | Blazor app the integration tests drive; renders one component per run and sweeps its surface |
How the suites divide the work
| Behavior | Where it's tested |
|---|---|
| Property → rendered attribute (direct-render components) | unit: plain bUnit facts (cut.Find(...).GetAttribute(...)) |
| Markup/child content rendering | unit: plain bUnit facts |
| Message-borne state serialization shapes | unit: PropertySerializationTests / EnumSerializationTests / RenderingSerializationTests |
| Interop wire behavior — method identifiers, argument serialization + type tags, return decoding, event-handler registration/removal transmissions + JS→.NET event dispatch, interop-borne props/data | unit: the component's ComponentContract — full authoring guide: references/interop-contracts.md |
<Member>Script parameters (JS-side handlers/providers) |
unit: automated sweep (ScriptPropTests, all components at once — no per-contract entries, no integration coverage exists) |
| End-to-end prop/event/method behavior against the real web component in a real browser | integration: the TestBed sweep |
| Visual output, client-side component logic | integration / e2e — never unit |
The two suites overlap on purpose but answer different questions: integration proves the full pipeline works end-to-end (but can't attribute a failure to a side of the boundary, and skips everything excluded in componentsConfig.json); interop contracts pin the .NET side of the wire protocol at unit speed, per member — including the members integration excludes — and can accommodate different implementations via the InteropHarness seam (InteropHarnessRegistry swaps stacks per component).
Unit suite (tests/IgniteUI.Blazor.Tests)
Base classes
BlazorComponentTestBase— bUnitBunitContextwith setupIIgniteUIBlazorservice, a recording JS runtime (JSRuntimeMode.Loose; every invocation recorded) and anInteropharness property (anInteropHarness, resolved per component type viaInteropFor<TComponent>()). Default base for suites without an interop contract.ComponentWithContractTestBase<TComponent>— adds a declarativeComponentContract<TComponent>,protectedrunners (VerifyMethodContract/VerifyPropContract/VerifyEventContract) that the suite exposes as one-liner[Fact]s, and an inheritedContract_SectionsHaveFactsguard that fails if a non-empty contract section has no runner fact. Exactly one suite per component carries the contract; sync/async method pairs are declared together via the twin overloads.
Running
dotnet test tests/IgniteUI.Blazor.Tests -f net10.0 --nologo --filter "FullyQualifiedName~<Name>Tests"
- Iterate on one TFM (
-f net10.0); finish with a full run on all TFMs (drop-f). - Contract failure stacks carry the violated contract line as the top frame — the test UI navigates to the spec, not the shared runner.
Conventions
- Generally follow one file per component:
<Name>Tests.csholds<Name>Tests. A component's child components may share the parent's file (SelectTests.csalso holdsSelectItemTests/SelectGroupTests/SelectHeaderTests; likewise Card, List, Dropdown, NavDrawer, Tabs, Stepper, TileManager, Tree, Rating, Slider) — that's the only permitted multi-class file. Cross-cutting suites (BaseControlTests,*SerializationTests,ScriptPropTests,Interop*Tests) are keyed to a class/behavior, not a component, and keep their own files. - New facts go into the existing
<Name>Testsclass; if the class carries a contract, plain bUnit facts sit alongside the contract runners in the same class. - Shared test helpers live in
ElementAssertionExtensions.cs. - Skips need a mechanism reason. Bug-tracker or
componentsConfig.jsonlistings are not skip reasons. If a member's decode hits a gap in shared infrastructure the change doesn't own, cover it pinning current behavior and write the correct assertion commented out under a// TODO:explaining the gap — ready to uncomment when fixed. A gap in a component under construction is a bug to fix, not to pin — see the two authoring modes in the interop reference. - Components are
partial— the generatedsrc/components/Blazor/<Name>.csis not the whole class; always checksrc/componentsBase/WebInputs/<Name>.csfor hand-written extensions before drawing conclusions. - Formatting: only
dotnet format whitespace --folderis safe in this repo — a fulldotnet formatwrites conflict markers into multi-targeted sources.
Integration suite (Playwright + TestBed)
One generic NUnit test per component (ComponentTest, fixture-sourced from the TestBed's
component list): it boots the TestBed app (in-memory by default), calls the browser-side
renderComponent('<IgbName>'), and asserts getErrors() returns nothing. The real work
happens inside the TestBed (Components/Pages/Home.razor): a reflection-driven sweep
over the component's public surface —
TestProps: sets each[Parameter]server-side with generated sample values and compares against the live web component's DOM property in the browser (TestUtil.PropertyValuesAreEqual, client value viastringifyObject).TestEvents: dispatches synthetic events against the web component and verifies the bound .NET callbacks fire.- Method spies (
spyOnMethod/checkOnSpyinwwwroot/app.js): invoking the .NET API must call the matching web-component method with the right args/result. Async methods only — the sweep filters to*Asyncand also excludesGet*Asyncgetters; sync variants can't run on the Server-hosted TestBed (no in-process JS runtime), so sync variants, getters, and<Member>Scriptparams have no integration coverage — the unit suite is their only net.
componentsConfig.json (in the TestBed project) gates the sweep per component:
ExcludedProps, ExcludedEvents, DependantProps/DependantMethods — each entry usually cites a BUG number or a shape the generic sweep can't model. An excluded member has no integration coverage at all — treat those as higher priority for the unit interop contract, never as members to skip there too.
Running
- One-time: build, then install browsers —
pwsh tests/IgniteUI.Blazor.Lite.IntegrationTests/bin/Debug/net<version>/playwright.ps1 install chromium. dotnet test tests/IgniteUI.Blazor.Lite.IntegrationTests(or per component from the Test Explorer — fixtures are named by component)..runsettingsknobs: headless off for local debugging;useInMemoryClient: falseto run against your owndotnet runinstance of the TestBed.- Full setup details: tests/IgniteUI.Blazor.Lite.IntegrationTests/README.md.
Adding coverage for a new component — checklist
- Unit suite: bUnit facts for rendered attributes/markup; if the component has an interop surface (it sends interop invocations or registers JS-originated event handlers — the reference explains how to identify these per stack), add a
ComponentContractperreferences/interop-contracts.md. For a brand-new component this works TDD-style: author the contract first from the intended API following the library's conventions; the contract doubles as the API design record (spec mode in the reference). - Integration: the sweep picks the component up automatically; add
componentsConfig.jsonentries only for members the generic sweep genuinely cannot model (cite why), and mirror each exclusion with unit contract coverage. - Verify: unit full-suite on all TFMs, plus the component's integration fixture.