test: add interop unit tests - #259
Conversation
17c8218 to
138097f
Compare
def36a8 to
59ea61f
Compare
There was a problem hiding this comment.
Pull request overview
This PR adds unit-level test coverage for the Blazor↔JS interop layer in Ignite UI for Blazor without changing product source: it introduces an InteropHarness abstraction to observe/drive interop behavior, switches the test base to bUnit’s recording JS runtime, and adds targeted tests for readiness gating, JS→.NET event dispatch, method invocation/return decoding, and message-borne serialization.
Changes:
- Introduces an interop testing seam (
InteropHarness+ registry + legacy adapter) that normalizes recorded interop calls/state syncs and drives returns/events/readiness through publicWebCallbackJSInvokables. - Replaces the Moq-based “swallow everything” JS runtime with bUnit
JSRuntimeMode.Looseso interop invocations are recorded and assertable. - Adds new interop-focused unit tests covering readiness, event dispatch + two-way binding propagation, API method call shapes, promise returns, and structural JSON serialization/deltas.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| tests/IgniteUI.Blazor.Tests/BlazorComponentTestBase.cs | Switches the shared test context to bUnit’s recording JS interop and wires interop through the new harness seam. |
| tests/IgniteUI.Blazor.Tests/IgniteUI.Blazor.Tests.csproj | Removes Moq dependency now that bUnit’s recording JS runtime is used. |
| tests/IgniteUI.Blazor.Tests/Interop/InteropHarness.cs | Defines normalized interop observation models and the abstract harness API used by tests. |
| tests/IgniteUI.Blazor.Tests/Interop/InteropHarnessRegistry.cs | Adds a registry to map component types to harness implementations (defaulting to the legacy renderer-message harness). |
| tests/IgniteUI.Blazor.Tests/Interop/RendererMessageInteropHarness.cs | Implements the harness for the current renderer-message interop stack, including stubbing and invocation/state recording. |
| tests/IgniteUI.Blazor.Tests/InteropReadinessTests.cs | Adds readiness-gate coverage for pre-ready invocation behavior and both readiness paths. |
| tests/IgniteUI.Blazor.Tests/InteropEventTests.cs | Adds JS→.NET event dispatch tests, including typed args and two-way binding propagation. |
| tests/IgniteUI.Blazor.Tests/MethodInteropTests.cs | Adds method-invocation shape tests (args/types, identifier mapping, return decoding, promise completion, sync path). |
| tests/IgniteUI.Blazor.Tests/MessageSerializationTests.cs | Adds structural JSON assertions for serialized state and message-borne delta/full descriptions. |
Replace the swallow-everything Moq IJSRuntime with bUnit's recording runtime (loose mode) so every interop invocation is observable. All knowledge of the current wire format (igSendMessage RendererMessage envelopes, retType returns, WebCallback entry points) is centralized in RendererMessageInteropHarness behind the implementation-neutral InteropHarness seam; InteropHarnessRegistry allows remapping individual components to a different harness when the interop infrastructure is rewritten. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
59ea61f to
5bd5053
Compare
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
MayaKirova
left a comment
There was a problem hiding this comment.
LGTM, but I do hope we don't do any major changes to what the JS side sends in terms of format and params of messages to the server, since many of the mocked responses in the tests would break (like the ones mocked for the events or methods).
Yeah, I tried to squeeze the model to be as neutral as possible and hide most of the annoying bit behind the harness implementation. |
Interop migration plan: Phase 0 — contract tests.
Summary
Adds unit-level interop coverage for the whole Lite component set. Every component with an interop surface now carries a declarative interop contract (
ComponentContract<TComponent>) pinning how its public API maps onto the wire: method identifiers, argument serialization and type tags, return decoding (including component/data-item references), event-handler registration/removal transmissions plus JS→.NET event dispatch, and interop-borne props/data-source transfers. Plus shared semantics packs for readiness, event plumbing, the deferred return path, and message-borne serialization shapes.Zero product-source changes — everything is driven through already-public surface (
WebCallbackJSInvokables, rendereddata-ig-idmarkers, recordedigSendMessagepayloads). The previous Moq swallow-everythingIJSRuntimesetup (which never verified an invocation) is replaced by bUnit's recording runtime; Moq is removed.Design: the
InteropHarnessseamThe interop infrastructure is slated for a rewrite, with components migrating one at a time. Contracts therefore avoid the wire format directly:
SetupMethodResult/SetupPropertyRead,RaiseEvent,FindPropertyUpdate,PrimeReady/MakeReady,CompleteDeferred,ContainerIdOf.igSendMessage/igCheckReady/igWaitForLoaded,RendererMessageenvelopes (invokeMethod/description/descriptionDelta/refChanged+dataRefindirection),retTypereturn encoding, barerefTypereference returns,"p:"property reads,WebCallbackentry points,data-ig-iddiscovery.Register<IgbX>(...)— its contract runs unchanged and becomes the migration acceptance gate.The contract DSL (per-suite)
Assert.Same), data items as uuid refs, single bound-object returns (InteropReturn.Ref— bare, no envelope), and hosted getters (ContractHost.Of<IgbTree>+target:) for child components that only exist inside a parent (TreeItem'sGetPathAsync).X()/XAsync()pair is declared together (async selector, sync twin selector); the runner re-invokes the sync twin against the same wire/decode expectations, so manually-maintained twins can't drift apart silently. Sync variants have no other coverage anywhere — the Server-hosted integration TestBed only runs*Asyncmethods.[Obsolete(error: true)]overloads catch a void method growing a return (either twin), and the bare.Event(c => c.Closing)form only compiles forEventCallback<IgbVoidEventArgs>— a void event growing data demands a payload.Methods_/Props_/Events_FollowContract) keep navigation in the suite; an inherited guard fact (Contract_SectionsHaveFacts) fails if a non-empty contract section has no runner fact.<Member>Scriptparameters (140 across the library, previously uncovered anywhere) are swept generically (ScriptPropTests): one reflection-enumerated theory per component asserts each script prop transmits a script reference for its target member — uniform generated surface, so no per-contract entries.skills/igniteui-blazor-lite-testing/references/interop-contracts.md(part of the newigniteui-blazor-lite-testingrepo skill, which also documents the Playwright/TestBed integration setup).Relation to the integration suite (TestBed)
Overlap is intentional but the axes differ:
componentsConfig.json(ExcludedProps/DependantMethods/ExcludedEvents).Product gaps found and pinned (not skipped)
Contracting the whole surface surfaced real defects; each is covered pinning current behavior with the correct assertion commented out under a
// TODO:(grepTODOin the test project), ready to uncomment when fixed:IgbStep,IgbSelectGroup,IgbDropdownGrouphave no parent-registration wiring —GetSteps/GetGroupsrefs always decode to null elements.IgbTreeItempath refs resolve only the self-reference — a nested item'sGetPathAsyncyields[null, self](reproduced in-app).ChangeTypenever decodes (wire keytypevschangeType); numeric-key combos needT=double/object(boxed-double unbox).MarshalByValueFactorylacks aWebDateRangeValueentry —DateRangePicker.GetCurrentValueAsyncthrows on any non-null reply;SelectAsync(range)sends a meaningless element ref instead of{start, end}.RendererSerializer.AddArrayPropvalues as object[]cast breaks struct-element arrays (int[],double[]).defaultarrivesFactory.Create-wrapped (Receiver set, Delegate null), fails the setter'sEmptycheck, and NREs inCompareEventCallbacksonleftDelegate.Equals(...); a rawEmpty(programmaticSetParameters) reaches the generated unset branch and NREs inOnRefChangedonnewValue.ToString().Verification
dotnet test tests/IgniteUI.Blazor.Tests— green on all TFMs (net8.0/net9.0/net10.0): 924 passed, 22 skipped (pre-existing), 0 failed per TFM. Contract runs are stub-driven and deterministic; the recording-concurrency path is snapshot-with-retry hardened.🤖 Generated with Claude Code