Skip to content

test: add interop unit tests - #259

Merged
damyanpetev merged 9 commits into
masterfrom
dpetev/interop-unit-tests
Jul 28, 2026
Merged

test: add interop unit tests#259
damyanpetev merged 9 commits into
masterfrom
dpetev/interop-unit-tests

Conversation

@damyanpetev

@damyanpetev damyanpetev commented Jul 19, 2026

Copy link
Copy Markdown
Member

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 (WebCallback JSInvokables, rendered data-ig-id markers, recorded igSendMessage payloads). The previous Moq swallow-everything IJSRuntime setup (which never verified an invocation) is replaced by bUnit's recording runtime; Moq is removed.

Design: the InteropHarness seam

The interop infrastructure is slated for a rewrite, with components migrating one at a time. Contracts therefore avoid the wire format directly:

tests/IgniteUI.Blazor.Tests/Interop/
├── InteropHarness.cs                 # abstract seam + normalized models (InteropMethodCall,
│                                     #   InteropReturn incl. Object/Array/Ref/Deferred)
├── RendererMessageInteropHarness.cs  # THE place that knows the current wire format
├── InteropHarnessRegistry.cs         # component type → harness factory (default: legacy)
└── ComponentContract.cs              # declarative contract DSL + ContractHost
  • Tests speak implementation-neutral terms: SetupMethodResult/SetupPropertyRead, RaiseEvent, FindPropertyUpdate, PrimeReady/MakeReady, CompleteDeferred, ContainerIdOf.
  • The legacy adapter concentrates every wire detail: igSendMessage/igCheckReady/igWaitForLoaded, RendererMessage envelopes (invokeMethod/description/descriptionDelta/refChanged + dataRef indirection), retType return encoding, bare refType reference returns, "p:" property reads, WebCallback entry points, data-ig-id discovery.
  • When a component moves to the new stack: implement a new harness, Register<IgbX>(...) — its contract runs unchanged and becomes the migration acceptance gate.

The contract DSL (per-suite)

protected override ComponentContract<IgbCombo<Item>> Contract { get; } = new ComponentContract<IgbCombo<Item>>()
    .Method(c => c.ShowAsync(), c => c.Show(), "show", returns: true)        // wire id, args/types, return round-trip — sync twin held to the same expectations
    .Getter(c => c.GetSelectionAsync(), "Selection", arrange, returns, assert) // "p:" reads, ref-array decoding
    .Event(c => c.Change, arrange, argsJson, assert)                          // JS payload → typed args → handler
    .Prop(c => c.Open, true)                                                  // interop-borne props (wire name derived)
    .Prop(c => c.Data, items, wire: new JsonSubset("""[...]"""));             // data-source transfers via dataRef
  • Arranged overloads cover reference semantics: child components resolved back to .NET instances (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's GetPathAsync).
  • Sync/async twins: every 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 *Async methods.
  • Drift is a build break, not a green run: poisoned [Obsolete(error: true)] overloads catch a void method growing a return (either twin), and the bare .Event(c => c.Closing) form only compiles for EventCallback<IgbVoidEventArgs> — a void event growing data demands a payload.
  • Failures name the violated member and carry the declaring contract line as the top stack frame — test UIs navigate to the spec, not the shared runner. Per-suite one-liner facts (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>Script parameters (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.
  • Authoring guide: skills/igniteui-blazor-lite-testing/references/interop-contracts.md (part of the new igniteui-blazor-lite-testing repo skill, which also documents the Playwright/TestBed integration setup).

Relation to the integration suite (TestBed)

Overlap is intentional but the axes differ:

  • Integration proves end-to-end behavior in a real browser — real client code, real DOM effects. It cannot attribute a failure to a side of the boundary, is slow, and skips everything listed in componentsConfig.json (ExcludedProps/DependantMethods/ExcludedEvents).
  • Contracts pin the .NET side of the wire protocol at unit speed (ms, no browser): exact identifiers, serialized shapes, type tags, decode behavior — per instance, per member. They cover the config-excluded members (which previously had no coverage at all), and they survive the interop rewrite via the seam, turning migration into a per-component green-bar exercise.
  • What contracts deliberately do NOT cover: client-side logic and rendering effects (integration/e2e), attribute rendering (existing bUnit facts).

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: (grep TODO in the test project), ready to uncomment when fixed:

  • IgbStep, IgbSelectGroup, IgbDropdownGroup have no parent-registration wiring — GetSteps/GetGroups refs always decode to null elements.
  • IgbTreeItem path refs resolve only the self-reference — a nested item's GetPathAsync yields [null, self] (reproduced in-app).
  • Combo ChangeType never decodes (wire key type vs changeType); numeric-key combos need T=double/object (boxed-double unbox).
  • MarshalByValueFactory lacks a WebDateRangeValue entry — DateRangePicker.GetCurrentValueAsync throws on any non-null reply; SelectAsync(range) sends a meaningless element ref instead of {start, end}.
  • RendererSerializer.AddArrayProp values as object[] cast breaks struct-element arrays (int[], double[]).
  • Unbinding an event callback crashes on both construction paths: a Razor-bound default arrives Factory.Create-wrapped (Receiver set, Delegate null), fails the setter's Empty check, and NREs in CompareEventCallbacks on leftDelegate.Equals(...); a raw Empty (programmatic SetParameters) reaches the generated unset branch and NREs in OnRefChanged on newValue.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

@damyanpetev
damyanpetev force-pushed the dpetev/formatting-setup branch from 17c8218 to 138097f Compare July 21, 2026 17:23
Base automatically changed from dpetev/formatting-setup to master July 22, 2026 12:09
@damyanpetev
damyanpetev force-pushed the dpetev/interop-unit-tests branch from def36a8 to 59ea61f Compare July 22, 2026 12:19
Copilot AI review requested due to automatic review settings July 22, 2026 12:19

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 public WebCallback JSInvokables.
  • Replaces the Moq-based “swallow everything” JS runtime with bUnit JSRuntimeMode.Loose so 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.

@damyanpetev damyanpetev changed the title Dpetev/interop unit tests Interop unit tests Jul 22, 2026
@damyanpetev damyanpetev changed the title Interop unit tests test: add Interop unit tests Jul 22, 2026
@damyanpetev damyanpetev changed the title test: add Interop unit tests test: add interop unit tests Jul 22, 2026
@damyanpetev damyanpetev added 🧪 ci: tests squash-merge Merge PR with "Squash and Merge" option labels Jul 22, 2026
damyanpetev and others added 6 commits July 27, 2026 21:22
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>
@damyanpetev
damyanpetev force-pushed the dpetev/interop-unit-tests branch from 59ea61f to 5bd5053 Compare July 27, 2026 18:29
@damyanpetev
damyanpetev requested a review from MayaKirova July 27, 2026 18:29
@damyanpetev
damyanpetev marked this pull request as ready for review July 27, 2026 18:34
@damyanpetev
damyanpetev requested a review from Copilot July 27, 2026 18:35

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 47 out of 47 changed files in this pull request and generated 3 comments.

Comment thread skills/igniteui-blazor-lite-testing/references/interop-contracts.md
Comment thread tests/IgniteUI.Blazor.Tests/BlazorComponentTestBase.cs
Comment thread skills/igniteui-blazor-lite-testing/SKILL.md Outdated
damyanpetev and others added 2 commits July 28, 2026 09:03
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 MayaKirova left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

@damyanpetev

Copy link
Copy Markdown
Member Author

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.
That being said, sadly, some of the impl. details are still in there, such as by-id marshalling refs and such. Ultimately, no idea how those will turn out yet, but it should be minimal changes at least and that might be the best I can push for right now :)

@damyanpetev
damyanpetev enabled auto-merge (squash) July 28, 2026 15:35
@damyanpetev
damyanpetev merged commit 0f90a04 into master Jul 28, 2026
7 checks passed
@damyanpetev
damyanpetev deleted the dpetev/interop-unit-tests branch July 28, 2026 15:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

squash-merge Merge PR with "Squash and Merge" option 🧪 ci: tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants