From d428fbbc97400d0094e0d52fadf305c266f33556 Mon Sep 17 00:00:00 2001 From: damyanpetev Date: Mon, 3 Aug 2026 17:28:12 +0300 Subject: [PATCH] test: consolidate interop contract DSL and unify the spec runner Co-Authored-By: Claude --- .../references/interop-contracts.md | 17 +- tests/IgniteUI.Blazor.Tests/AccordionTests.cs | 8 +- tests/IgniteUI.Blazor.Tests/CalendarTests.cs | 2 +- tests/IgniteUI.Blazor.Tests/ChatTests.cs | 2 +- tests/IgniteUI.Blazor.Tests/ComboTests.cs | 20 +-- .../ComponentWithContractTestBase.cs | 126 ++++++------- tests/IgniteUI.Blazor.Tests/DropdownTests.cs | 12 +- .../Interop/ComponentContract.cs | 165 ++++++++++-------- tests/IgniteUI.Blazor.Tests/SelectTests.cs | 8 +- tests/IgniteUI.Blazor.Tests/StepperTests.cs | 2 +- tests/IgniteUI.Blazor.Tests/TabsTests.cs | 2 +- .../IgniteUI.Blazor.Tests/TileManagerTests.cs | 18 +- tests/IgniteUI.Blazor.Tests/TreeTests.cs | 16 +- 13 files changed, 210 insertions(+), 188 deletions(-) diff --git a/skills/igniteui-blazor-lite-testing/references/interop-contracts.md b/skills/igniteui-blazor-lite-testing/references/interop-contracts.md index 85865fdd..086e5dad 100644 --- a/skills/igniteui-blazor-lite-testing/references/interop-contracts.md +++ b/skills/igniteui-blazor-lite-testing/references/interop-contracts.md @@ -35,6 +35,19 @@ Read the component's source (all `partial` parts) and find every place it: The *concrete idioms* for all three depend on which interop stack the component currently sits on — the [cheat-sheet below](#current-stack-cheat-sheet-legacy-renderermessage-pipeline) lists them for the legacy `RendererMessage` pipeline. For a component on a different stack, find the equivalent call sites in its implementation (whatever sits between the public member and the JS runtime — a direct `IJSRuntime.InvokeAsync`, a channel service, etc.); the principles and the DSL below are unchanged. +### Values that only exist after the render — one mechanism, everywhere + +Interop instance ids, data-item uuids and element handles are assigned when the component renders, which is *after* the contract itself is constructed. Wherever a spec states such a value, the parameter is a `FromRender`: pass the value directly when it is fixed (the common case — nothing to think about), or `FromRender.Of((interop, cut) => …)` when it can only be settled once rendered. The DSL has exactly one such mechanism, so a new kind of late value needs no new API: + +```csharp +returns: FromRender.Of((interop, cut) => InteropReturn.Ref(...)) // a getter's stubbed return +args: [FromRender.Of((interop, cut) => $"containerId:::{...}")] // one late argument among fixed ones +argsJson: FromRender.Of((interop, cut) => ...) // an event payload carrying a child ref +wire: FromRender.Of((interop, cut) => new RawJson(...)) // a prop whose value references data items +``` + +The render scope handed to the lambda is the cut for an arranged spec and the whole host render for a hosted one — the same scope the spec's own `assert:` receives. Pair it with `arrange:` (or `host:`/`target:`) so there is something rendered to reference. + ### 1. Methods and getters - Every public API member that produces an outbound invocation gets a spec. Distinguish two kinds: @@ -53,7 +66,7 @@ The *concrete idioms* for all three depend on which interop stack the component (An explicit `InteropReturn` + separate `expect:` overload exists for wire returns that decode to a different value than sent — rare.) - **Value-returning methods must state their return** — enforced at compile time: a `Task`-returning lambda on the void `.Method` overload is `[Obsolete(error: true)]` as a guard, so the contract states the `returns:` stub. The void overload needs no stub. - **Hosted arrange — child components that only exist inside a parent** - Currently available in the hosted `Getter` overload (e.g. a tree item's `GetPathAsync` needs real ancestors). Build the full render with `ContractHost.Of(ps => ps.AddChildContent(...))` (parent as the root, structure nested inside), pick the component under test with `target: h => h.FindComponents()[n]`, and note that `returns:`/`assert:` receive the whole host render — so `interop.ContainerIdOf(h, "")` reaches ancestors and siblings outside the cut's subtree. The read still travels on the target's own container. See `TreeItemTests` in `TreeTests.cs`. -- **Render-sourced arguments — a value that only exists once rendered** (a sibling component instance, an `ElementReference` captured from markup): the spec's selectors run *after* its render, so read the value from a holder the `arrange:` render fills — `.Method(c => c.ShowAsync(anchor.Component), ...)` — rather than reaching for a new overload. The expected *wire* value comes the same way: `args: [new FromRender((interop, cut) => ...)]` is resolved right before comparison, when child instance ids exist. `elements:` states the element handles the invocation must carry alongside its arguments; omitting it asserts the invocation carries none — so a component-typed target is positively pinned as crossing without an element handle. Both `arrange:` and `elements:` are available on the void and the value-returning twin overloads. See the `show`/`toggle` target overloads in `DropdownTests.cs`. +- **Render-sourced arguments — a value that only exists once rendered** (a sibling component instance, an `ElementReference` captured from markup): the spec's selectors run *after* its render, so read the value from a holder the `arrange:` render fills — `.Method(c => c.ShowAsync(anchor.Component), ...)` — rather than reaching for a new overload. The expected *wire* value comes the same way: `args: [FromRender.Of((interop, cut) => ...)]` is settled right before comparison, when child instance ids exist. `elements:` states the element handles the invocation must carry alongside its arguments; omitting it asserts the invocation carries none — so a component-typed target is positively pinned as crossing without an element handle. Both `arrange:` and `elements:` are available on the void and the value-returning twin overloads. See the `show`/`toggle` target overloads in `DropdownTests.cs`. - **Sync twins are declared inside the member's spec**: every `X()`/`XAsync()` pair uses the twin overloads — both selectors up front, async first: `.Method(c => c.ShowAsync(), c => c.Show(), "show", returns: true)` (same for `.Getter`, including the arranged/hosted forms). The runner re-invokes the sync twin against the same expectations (identifier, args, types, decoded return). Declaring the twin is the contract author's responsibility — when enumerating methods, treat an `X()`/`XAsync()` pair as one member and use the twin overload. Sync dispatch rides the in-process JS runtime (`IJSInProcessRuntime` — WASM/WebView-only in production; the Server-hosted integration TestBed can't run sync variants, so contracts are their only coverage). - **Skip and list**: a member whose decode hits an implementation gap (unregistered child type, missing marshal-by-value entry) is covered, not skipped — per the authoring modes above. @@ -77,7 +90,7 @@ The contract entry is the member selector plus a sample value: the wire name is - A property belongs in the contract **only when its value travels over interop** rather than as a rendered attribute. Determine which from the component's serialization path (stack-specific — see the cheat-sheet) or empirically: for an attribute prop, the runner fails with *"no property update transmission was observed"*. Attribute props are covered by the suite's attribute facts instead. - **Serialized config objects and arrays**: set an instance with 2–3 distinctive values and expect a `JsonSubset` — subset match, extra bookkeeping fields on the transmitted value are ignored; arrays compare element-wise (equal length, per-element subset). See `ChatTests.cs`, `CalendarTests.cs`. - **Data sources** (`Data`/`DataSource` props): covered by `.Prop(c => c.Data, ...)` too — the harness follows whatever indirection the stack uses to the actual transfer. Item property names cross with their .NET names (not camelized). See `ComboTests.cs`. -- **Props referencing data items** (e.g. Combo's `Value`): use the arranged overload — arrange the `Data` the value points into and state the wire value as a factory, since tracked items cross as refs whose ids are only assigned on transfer: `.Prop(c => c.Value, [_item1], arrange: ps => ps.Add(c => c.Data, items), wire: (interop, cut) => new RawJson(...))`. +- **Props referencing data items** (e.g. Combo's `Value`): add `arrange:` for the `Data` the value points into and state the wire value late, since tracked items cross as refs whose ids are only assigned on transfer: `.Prop(c => c.Value, [_item1], wire: FromRender.Of((interop, cut) => new RawJson(...)), arrange: ps => ps.Add(c => c.Data, items))`. ## Current-stack cheat-sheet (legacy `RendererMessage` pipeline) diff --git a/tests/IgniteUI.Blazor.Tests/AccordionTests.cs b/tests/IgniteUI.Blazor.Tests/AccordionTests.cs index 4eae341b..1e49e5ed 100644 --- a/tests/IgniteUI.Blazor.Tests/AccordionTests.cs +++ b/tests/IgniteUI.Blazor.Tests/AccordionTests.cs @@ -23,19 +23,19 @@ public class AccordionTests : ComponentWithContractTestBase .Method(c => c.ShowAllAsync(), c => c.ShowAll(), "showAll") .Event(c => c.Opening, arrange, - argsJson: (interop, cut) => $$$"""{"detail": {"refType": "name", "id": "{{{interop.ContainerIdOf(cut, "igc-expansion-panel:nth-of-type(2)")}}}"}}""", + argsJson: FromRender.Of((interop, cut) => $$$"""{"detail": {"refType": "name", "id": "{{{interop.ContainerIdOf(cut, "igc-expansion-panel:nth-of-type(2)")}}}"}}"""), assert: (cut, args) => Assert.Same(cut.FindComponents()[1].Instance, args.Detail)) .Event(c => c.Opened, arrange, - argsJson: (interop, cut) => $$$"""{"detail": {"refType": "name", "id": "{{{interop.ContainerIdOf(cut, "igc-expansion-panel:nth-of-type(2)")}}}"}}""", + argsJson: FromRender.Of((interop, cut) => $$$"""{"detail": {"refType": "name", "id": "{{{interop.ContainerIdOf(cut, "igc-expansion-panel:nth-of-type(2)")}}}"}}"""), assert: (cut, args) => Assert.Same(cut.FindComponents()[1].Instance, args.Detail)) .Event(c => c.Closing, arrange, - argsJson: (interop, cut) => $$$"""{"detail": {"refType": "name", "id": "{{{interop.ContainerIdOf(cut, "igc-expansion-panel:nth-of-type(2)")}}}"}}""", + argsJson: FromRender.Of((interop, cut) => $$$"""{"detail": {"refType": "name", "id": "{{{interop.ContainerIdOf(cut, "igc-expansion-panel:nth-of-type(2)")}}}"}}"""), assert: (cut, args) => Assert.Same(cut.FindComponents()[1].Instance, args.Detail)) .Event(c => c.Closed, arrange, - argsJson: (interop, cut) => $$$"""{"detail": {"refType": "name", "id": "{{{interop.ContainerIdOf(cut, "igc-expansion-panel:nth-of-type(2)")}}}"}}""", + argsJson: FromRender.Of((interop, cut) => $$$"""{"detail": {"refType": "name", "id": "{{{interop.ContainerIdOf(cut, "igc-expansion-panel:nth-of-type(2)")}}}"}}"""), assert: (cut, args) => Assert.Same(cut.FindComponents()[1].Instance, args.Detail)); [Fact] diff --git a/tests/IgniteUI.Blazor.Tests/CalendarTests.cs b/tests/IgniteUI.Blazor.Tests/CalendarTests.cs index 84911a55..201678ad 100644 --- a/tests/IgniteUI.Blazor.Tests/CalendarTests.cs +++ b/tests/IgniteUI.Blazor.Tests/CalendarTests.cs @@ -11,7 +11,7 @@ public class CalendarTests : ComponentWithContractTestBase returns: new DateTime(2026, 3, 15, 0, 0, 0, DateTimeKind.Utc)) .Getter(c => c.GetCurrentValuesAsync(), c => c.GetCurrentValues(), "Values", arrange: _ => { }, - returns: (interop, cut) => InteropReturn.Array("""["2026-01-02T03:04:05.000Z", "2026-03-16T12:30:00.000Z"]"""), + returns: FromRender.Of((interop, cut) => InteropReturn.Array("""["2026-01-02T03:04:05.000Z", "2026-03-16T12:30:00.000Z"]""")), assert: (cut, result) => { Assert.Equal(2, result.Length); diff --git a/tests/IgniteUI.Blazor.Tests/ChatTests.cs b/tests/IgniteUI.Blazor.Tests/ChatTests.cs index 57aa0afa..2c9312b7 100644 --- a/tests/IgniteUI.Blazor.Tests/ChatTests.cs +++ b/tests/IgniteUI.Blazor.Tests/ChatTests.cs @@ -11,7 +11,7 @@ public class ChatTests : ComponentWithContractTestBase args: ["message-42"], types: ["String"]) .Getter(c => c.GetCurrentDraftMessageAsync(), c => c.GetCurrentDraftMessage(), "DraftMessage", arrange: _ => { }, - returns: (interop, cut) => InteropReturn.Object("", """{"text": "wip draft"}"""), + returns: FromRender.Of((interop, cut) => InteropReturn.Object("", """{"text": "wip draft"}""")), assert: (cut, result) => Assert.Equal("wip draft", result.Text)) .Event(c => c.TypingChange, argsJson: """{"detail": true}""", diff --git a/tests/IgniteUI.Blazor.Tests/ComboTests.cs b/tests/IgniteUI.Blazor.Tests/ComboTests.cs index 6acb3961..f7b0ba3f 100644 --- a/tests/IgniteUI.Blazor.Tests/ComboTests.cs +++ b/tests/IgniteUI.Blazor.Tests/ComboTests.cs @@ -47,17 +47,17 @@ internal static string ChangeDetail(string newValues, string items, string type args: ["invalid entry"], types: ["String"]) .Getter(c => c.GetCurrentValueAsync(), c => c.GetCurrentValue(), "Value", arrange: ps => ps.Add(c => c.Data, new[] { _valueItem1, _valueItem2 }), - returns: (interop, cut) => InteropReturn.Array( - $$"""[{"refType": "uuid", "id": "{{DataItemId(interop, cut, 0)}}"}]"""), + returns: FromRender.Of((interop, cut) => InteropReturn.Array( + $$"""[{"refType": "uuid", "id": "{{DataItemId(interop, cut, 0)}}"}]""")), assert: (cut, result) => Assert.Same(_valueItem1, Assert.Single(result))) .Getter(c => c.GetSelectionAsync(), c => c.GetSelection(), "Selection", arrange: ps => ps.Add(c => c.Data, new[] { _valueItem1, _valueItem2 }), - returns: (interop, cut) => InteropReturn.Array( - $$"""[{"refType": "uuid", "id": "{{DataItemId(interop, cut, 1)}}"}]"""), + returns: FromRender.Of((interop, cut) => InteropReturn.Array( + $$"""[{"refType": "uuid", "id": "{{DataItemId(interop, cut, 1)}}"}]""")), assert: (cut, result) => Assert.Same(_valueItem2, Assert.Single(result))) .Event(c => c.Change, arrange: ps => ps.Add(c => c.Data, new[] { _valueItem1, _valueItem2 }), - argsJson: (interop, cut) => ChangeDetail(UuidRef(interop, cut, 0), UuidRef(interop, cut, 0)), + argsJson: FromRender.Of((interop, cut) => ChangeDetail(UuidRef(interop, cut, 0), UuidRef(interop, cut, 0))), assert: (cut, args) => { Assert.Same(_valueItem1, Assert.Single(args.Detail.NewValue)); @@ -68,7 +68,7 @@ internal static string ChangeDetail(string newValues, string items, string type arrange: ps => ps .Add(c => c.Data, new[] { _valueItem1, _valueItem2 }) .Add(c => c.Value, [_valueItem1]), - argsJson: (interop, cut) => ChangeDetail("", UuidRef(interop, cut, 0), "deselection"), + argsJson: FromRender.Of((interop, cut) => ChangeDetail("", UuidRef(interop, cut, 0), "deselection")), assert: (cut, args) => { Assert.Empty(args.Detail.NewValue); @@ -79,9 +79,9 @@ internal static string ChangeDetail(string newValues, string items, string type }) .Event(c => c.Change, arrange: ps => ps.Add(c => c.Data, new[] { _valueItem1, _valueItem2 }), - argsJson: (interop, cut) => ChangeDetail( + argsJson: FromRender.Of((interop, cut) => ChangeDetail( UuidRef(interop, cut, 0) + ", " + UuidRef(interop, cut, 1), - UuidRef(interop, cut, 0) + ", " + UuidRef(interop, cut, 1)), + UuidRef(interop, cut, 0) + ", " + UuidRef(interop, cut, 1))), assert: (cut, args) => { // Multi-selection: every element resolves back to its original data instance. @@ -128,7 +128,7 @@ internal static string ChangeDetail(string newValues, string items, string type .Prop(c => c.Value, value: [_valueItem1], arrange: ps => ps.Add(c => c.Data, new[] { _valueItem1, _valueItem2 }), - wire: (interop, cut) => new RawJson($"[{UuidRef(interop, cut, 0)}]")) + wire: FromRender.Of((interop, cut) => new RawJson($"[{UuidRef(interop, cut, 0)}]"))) .Prop(c => c.Data, new[] { @@ -295,7 +295,7 @@ public class ComboValueKeyTests : ComponentWithContractTestBase protected override ComponentContract> InteropContract { get; } = new ComponentContract>() .Event(c => c.Change, arrange, - argsJson: (interop, cut) => ComboTests.ChangeDetail("2", ComboTests.UuidRef(interop, cut, 1)), + argsJson: FromRender.Of((interop, cut) => ComboTests.ChangeDetail("2", ComboTests.UuidRef(interop, cut, 1))), assert: (cut, args) => { Assert.Equal(2.0, Assert.Single(args.Detail.NewValue)); // numbers decode as double diff --git a/tests/IgniteUI.Blazor.Tests/ComponentWithContractTestBase.cs b/tests/IgniteUI.Blazor.Tests/ComponentWithContractTestBase.cs index 049c0b61..312bd4d7 100644 --- a/tests/IgniteUI.Blazor.Tests/ComponentWithContractTestBase.cs +++ b/tests/IgniteUI.Blazor.Tests/ComponentWithContractTestBase.cs @@ -128,14 +128,7 @@ protected async Task VerifyMethodContract() scope = cut; } - if (method.ReadsProperty is not null) - { - await RunGetterSpec(harness, cut, scope, method); - } - else - { - await RunMethodSpec(harness, cut, scope, method); - } + await RunSpec(harness, cut, scope, method); } catch (Exception ex) when (ex is not ContractViolationException) { @@ -147,39 +140,70 @@ protected async Task VerifyMethodContract() } } - /// Stub → invoke → assert the recorded call's identifier, args, and type tags, then the decoded return. - private static async Task RunMethodSpec( + /// + /// Stub → invoke → assert, for both kinds of spec. A current-state read differs from an + /// API call in only two ways — who names the wire identifier (the harness, from the + /// property name, vs. the spec) and that it carries no arguments — so the invocation, + /// sync-twin and return-decoding dance is written once, here. + /// + private static async Task RunSpec( InteropHarness harness, IRenderedComponent cut, IRenderedComponent scope, MethodContractSpec method) { - // Stub immediately before invoking so specs may reuse a method name + var isRead = method.ReadsProperty is not null; + + // Stub immediately before invoking so specs may reuse a member // with different stubbed results. - var stub = method.StubFactory?.Invoke(harness, scope) ?? method.Stub; - if (stub is not null) + var stub = method.Stub?.Get(harness, scope); + if (isRead) + { + harness.SetupPropertyRead(method.ReadsProperty!, stub!); + } + else if (stub is not null) { harness.SetupMethodResult(method.JsName!, stub); } var containerId = harness.ContainerIdOf(cut); - var (call, result) = await InvokeExpectingNewCall( - () => harness.CallsOf(method.JsName!, containerId), - () => method.Invoke(cut.Instance), - $"\"{method.JsName}\" sent no new invocation"); - AssertCallShape(harness, cut, method, call, result); - method.AssertReturnWithCut?.Invoke(scope, result); + // How a read is identified on the wire stays harness-owned; a call's identifier is the spec's. + Func> matching = isRead + ? () => harness.PropertyReads(containerId, method.ReadsProperty!) + : () => harness.CallsOf(method.JsName!, containerId); + var noNewCall = isRead + ? $"no new current-state read was issued for \"{method.ReadsProperty}\"" + : $"\"{method.JsName}\" sent no new invocation"; + + var (call, result) = await InvokeExpectingNewCall(matching, () => method.Invoke(cut.Instance), noNewCall); + AssertObserved(harness, cut, scope, method, call, result); if (method.SyncInvoke is not null) { - // The sync twin must produce the same invocation and decode the same reply + // The sync twin must produce its own invocation and decode the same reply // (the stub persists) to the same result. var (syncCall, syncResult) = await InvokeExpectingNewCall( - () => harness.CallsOf(method.JsName!, containerId), + matching, () => Task.FromResult(method.SyncInvoke(cut.Instance)), - $"sync twin \"{method.JsName}\" sent no new invocation"); - AssertCallShape(harness, cut, method, syncCall, syncResult); - method.AssertReturnWithCut?.Invoke(scope, syncResult); + "sync twin: " + noNewCall); + AssertObserved(harness, cut, scope, method, syncCall, syncResult); } } + /// Asserts everything the spec pins about one observed invocation: its wire shape (calls only) and its decoded return. + private static void AssertObserved( + InteropHarness harness, IRenderedComponent cut, IRenderedComponent scope, + MethodContractSpec method, InteropMethodCall call, object? result) + { + // A read carries no arguments, type tags or element handles — there is no wire shape to pin. + if (method.ReadsProperty is null) + { + AssertCallShape(harness, cut, method, call); + } + if (method.HasExpectedReturn) + { + AssertReturn(method.ExpectedReturn, result); + } + method.AssertReturnWithCut?.Invoke(scope, result); + } + /// /// Runs an invocation and returns the newest call it added to the matching set — /// requiring a NEW entry regardless of prior history, so specs may chain the same @@ -202,7 +226,7 @@ private static async Task RunMethodSpec( /// Asserts a recorded invocation matches the spec's expected args, type tags, and element handles. private static void AssertCallShape( - InteropHarness harness, IRenderedComponent cut, MethodContractSpec method, InteropMethodCall call, object? result) + InteropHarness harness, IRenderedComponent cut, MethodContractSpec method, InteropMethodCall call) { Assert.Equal(method.ExpectedTypes, call.Types); Assert.Equal(method.ExpectedArgs.Length, call.Arguments.Count); @@ -211,16 +235,11 @@ private static void AssertCallShape( AssertWireValue(Resolve(method.ExpectedArgs[i], harness, cut), call.Arguments[i]); } AssertElements(method.ExpectedElements?.Invoke() ?? [], call.Elements); - - if (method.HasExpectedReturn) - { - AssertReturn(method.ExpectedReturn, result); - } } - /// Resolves a expectation against the render; every other value is already final. - private static object? Resolve(object? expected, InteropHarness harness, IRenderedComponent cut) => - expected is FromRender fromRender ? fromRender.Value(harness, cut) : expected; + /// Settles a late expectation (see ) against the render; every other value is already final. + private static object? Resolve(object? expected, InteropHarness harness, IRenderedComponent scope) => + expected is IFromRender late ? late.Resolve(harness, scope) : expected; /// /// Asserts the element handles riding with the invocation, by id — a handle's id is @@ -241,40 +260,6 @@ private static void AssertElements(IReadOnlyList expected, IRe } } - /// Stub the property's JS-side value → invoke → assert a current-state read was issued and the return decoded. - private static async Task RunGetterSpec( - InteropHarness harness, IRenderedComponent cut, IRenderedComponent scope, MethodContractSpec method) - { - var stub = method.StubFactory?.Invoke(harness, scope) ?? method.Stub; - harness.SetupPropertyRead(method.ReadsProperty!, stub!); - - var containerId = harness.ContainerIdOf(cut); - var (_, result) = await InvokeExpectingNewCall( - () => harness.PropertyReads(containerId, method.ReadsProperty!), - () => method.Invoke(cut.Instance), - "no new current-state read was issued for the property"); - if (method.HasExpectedReturn) - { - AssertReturn(method.ExpectedReturn, result); - } - method.AssertReturnWithCut?.Invoke(scope, result); - - if (method.SyncInvoke is not null) - { - // The sync twin must issue its own read (the read's wire identifier stays - // harness-owned) and decode the persisting stub to the same result. - var (_, syncResult) = await InvokeExpectingNewCall( - () => harness.PropertyReads(containerId, method.ReadsProperty!), - () => Task.FromResult(method.SyncInvoke(cut.Instance)), - $"sync twin issued no new current-state read for \"{method.ReadsProperty}\""); - if (method.HasExpectedReturn) - { - AssertReturn(method.ExpectedReturn, syncResult); - } - method.AssertReturnWithCut?.Invoke(scope, syncResult); - } - } - /// /// Runner for the contract's .Prop specs — expose it on the suite as /// [Fact] public void Props_FollowContract() => VerifyPropContract();. @@ -309,10 +294,7 @@ protected void VerifyPropContract() "no property update transmission was observed — check the wire name, " + "and whether this prop crosses as a rendered attribute instead (not a .Prop case)"); } - var expected = prop.ExpectedValueFactory is not null - ? prop.ExpectedValueFactory(harness, cut) - : prop.ExpectedValue; - AssertWireValue(expected, actual.Value); + AssertWireValue(prop.ExpectedValue.Get(harness, cut), actual.Value); } catch (Exception ex) when (ex is not ContractViolationException) { @@ -367,7 +349,7 @@ protected void VerifyEventContract() ?? throw new XunitException("no event-handler registration transmission was observed"); Assert.Equal(evt.EventName, registration.GetString()); - var argsJson = evt.ArgsJsonFactory?.Invoke(harness, cut) ?? evt.ArgsJson; + var argsJson = evt.ArgsJson.Get(harness, cut); harness.RaiseEvent(containerId, evt.EventName, argsJson); if (received is null) diff --git a/tests/IgniteUI.Blazor.Tests/DropdownTests.cs b/tests/IgniteUI.Blazor.Tests/DropdownTests.cs index 35128e9c..ac101ba3 100644 --- a/tests/IgniteUI.Blazor.Tests/DropdownTests.cs +++ b/tests/IgniteUI.Blazor.Tests/DropdownTests.cs @@ -44,8 +44,8 @@ sealed class Anchor }); /// The wire form of the arranged component anchor — its interop instance id, assigned on render - static readonly FromRender componentAnchorArg = - new((interop, cut) => $"containerId:::{interop.ContainerIdOf(cut, "igc-button")}"); + static readonly FromRender componentAnchorArg = + FromRender.Of((interop, cut) => $"containerId:::{interop.ContainerIdOf(cut, "igc-button")}"); /// Arranges two IgbDropdownItem children static readonly Action> itemsArrange = @@ -96,7 +96,7 @@ sealed class Anchor InteropReturn.Undefined, expect: null!, args: [2.0], types: ["Json"]) .Getter(c => c.GetItemsAsync(), c => c.GetItems(), "Items", itemsArrange, - returns: (interop, cut) => InteropReturn.Array($$$"""[{"refType": "name", "id": "{{{interop.ContainerIdOf(cut, "igc-dropdown-item:nth-of-type(1)")}}}"}, {"refType": "name", "id": "{{{interop.ContainerIdOf(cut, "igc-dropdown-item:nth-of-type(2)")}}}"}]"""), + returns: FromRender.Of((interop, cut) => InteropReturn.Array($$$"""[{"refType": "name", "id": "{{{interop.ContainerIdOf(cut, "igc-dropdown-item:nth-of-type(1)")}}}"}, {"refType": "name", "id": "{{{interop.ContainerIdOf(cut, "igc-dropdown-item:nth-of-type(2)")}}}"}]""")), assert: (cut, result) => { Assert.Equal(2, result.Length); @@ -105,7 +105,7 @@ sealed class Anchor }) .Getter(c => c.GetGroupsAsync(), c => c.GetGroups(), "Groups", groupsArrange, - returns: (interop, cut) => InteropReturn.Array($$$"""[{"refType": "name", "id": "{{{interop.ContainerIdOf(cut, "igc-dropdown-group:nth-of-type(1)")}}}"}, {"refType": "name", "id": "{{{interop.ContainerIdOf(cut, "igc-dropdown-group:nth-of-type(2)")}}}"}]"""), + returns: FromRender.Of((interop, cut) => InteropReturn.Array($$$"""[{"refType": "name", "id": "{{{interop.ContainerIdOf(cut, "igc-dropdown-group:nth-of-type(1)")}}}"}, {"refType": "name", "id": "{{{interop.ContainerIdOf(cut, "igc-dropdown-group:nth-of-type(2)")}}}"}]""")), assert: (cut, result) => { Assert.Equal(2, result.Length); @@ -118,7 +118,7 @@ sealed class Anchor .Getter(c => c.GetSelectedItemAsync(), c => c.GetSelectedItem(), "SelectedItem", InteropReturn.Undefined, expect: null!) .Getter(c => c.GetSelectedItemAsync(), c => c.GetSelectedItem(), "SelectedItem", itemsArrange, - returns: (interop, cut) => InteropReturn.Ref($$"""{"refType": "name", "id": "{{interop.ContainerIdOf(cut, "igc-dropdown-item:nth-of-type(1)")}}"}"""), + returns: FromRender.Of((interop, cut) => InteropReturn.Ref($$"""{"refType": "name", "id": "{{interop.ContainerIdOf(cut, "igc-dropdown-item:nth-of-type(1)")}}"}""")), assert: (cut, result) => Assert.Same(cut.FindComponents()[0].Instance, result)) .Event(c => c.Opening) .Event(c => c.Opened) @@ -126,7 +126,7 @@ sealed class Anchor .Event(c => c.Closed) .Event(c => c.Change, itemsArrange, - argsJson: (interop, cut) => $$$"""{"detail": {"refType": "name", "id": "{{{interop.ContainerIdOf(cut, "igc-dropdown-item:nth-of-type(2)")}}}"}}""", + argsJson: FromRender.Of((interop, cut) => $$$"""{"detail": {"refType": "name", "id": "{{{interop.ContainerIdOf(cut, "igc-dropdown-item:nth-of-type(2)")}}}"}}"""), assert: (cut, args) => Assert.Same(cut.FindComponents()[1].Instance, args.Detail)); [Fact] diff --git a/tests/IgniteUI.Blazor.Tests/Interop/ComponentContract.cs b/tests/IgniteUI.Blazor.Tests/Interop/ComponentContract.cs index f30fdcaf..445744ad 100644 --- a/tests/IgniteUI.Blazor.Tests/Interop/ComponentContract.cs +++ b/tests/IgniteUI.Blazor.Tests/Interop/ComponentContract.cs @@ -41,12 +41,53 @@ public sealed record RawJson(string Json); public sealed record JsonSubset(string Json); /// -/// An expected wire value that can only be computed once the component has rendered — -/// typically a reference to a child, whose interop instance id is assigned then. Resolved -/// against the harness and the render right before comparison, and may itself produce a -/// / or a scalar. +/// Non-generic view of a , so the runner can spot and resolve a +/// late value sitting inside an untyped collection (a method's expected arguments). /// -public sealed record FromRender(Func, object?> Value); +internal interface IFromRender +{ + object? Resolve(InteropHarness harness, IRenderedComponent scope); +} + +/// +/// A contract value that can only be settled once the component has rendered — a child's +/// interop instance id, a captured element handle, a data item's assigned uuid. This is the +/// only such mechanism in the DSL: any parameter typed accepts +/// either a fixed value (implicitly, written exactly as it would be otherwise) or a late one +/// built with . The render scope is the cut for an arranged +/// spec and the whole host render for a hosted one, matching the spec's own scope. +/// +public readonly struct FromRender : IFromRender +{ + private readonly T _value; + private readonly Func, T>? _resolve; + + internal FromRender(T value) + { + _value = value; + _resolve = null; + } + + internal FromRender(Func, T> resolve) + { + _value = default!; + _resolve = resolve; + } + + public static implicit operator FromRender(T value) => new(value); + + /// The settled value: computed against the render when declared late, otherwise as given. + internal T Get(InteropHarness harness, IRenderedComponent scope) => + _resolve is null ? _value : _resolve(harness, scope); + + object? IFromRender.Resolve(InteropHarness harness, IRenderedComponent scope) => Get(harness, scope); +} + +/// Builds late values — the counterpart to . +public static class FromRender +{ + public static FromRender Of(Func, T> resolve) => new(resolve); +} public sealed class MethodContractSpec where TComponent : IComponent { @@ -59,7 +100,8 @@ public sealed class MethodContractSpec where TComponent : IComponent public required Func> Invoke { get; init; } public object?[] ExpectedArgs { get; init; } = []; public string[] ExpectedTypes { get; init; } = []; - public InteropReturn? Stub { get; init; } + /// The value the JS side hands back, settled against the render when the spec declared it late. + public FromRender? Stub { get; init; } public object? ExpectedReturn { get; init; } public bool HasExpectedReturn { get; init; } @@ -81,14 +123,7 @@ public sealed class MethodContractSpec where TComponent : IComponent /// public Func? SyncInvoke { get; init; } - /// - /// Dynamic stub for returns referencing arranged children (ids only known after render); - /// wins over . The fragment is the render scope: the cut itself for - /// arranged specs, the whole host for hosted ones (so ancestors are reachable). - /// - public Func, InteropReturn>? StubFactory { get; init; } - - /// Dynamic return assert receiving the render scope (see ) to compare against arranged instances. + /// Dynamic return assert receiving the render scope, to compare against arranged instances. public Action, object?>? AssertReturnWithCut { get; init; } /// @@ -105,14 +140,12 @@ public sealed class StatePropContractSpec where TComponent : ICompon { public required string WireName { get; init; } public required Action> Set { get; init; } - public object? ExpectedValue { get; init; } + /// The transmitted value, settled against the render when the spec declared it late. + public FromRender ExpectedValue { get; init; } /// Extra render setup the transmission depends on (e.g. data items the value must reference). public Action>? Arrange { get; init; } - /// Dynamic wire value for transmissions referencing arranged state (ids only known after render); wins over . - public Func, object?>? ExpectedValueFactory { get; init; } - public SpecSource? Source { get; init; } } @@ -133,7 +166,8 @@ public sealed class EventContractSpec where TComponent : IComponent /// The declared event args type; the runner asserts the received args are assignable to it. public required Type ArgsType { get; init; } - public string ArgsJson { get; init; } = "{}"; + /// The dispatched payload, settled against the render when the spec declared it late. + public FromRender ArgsJson { get; init; } = "{}"; public Action? AssertArgs { get; init; } /// Like but also receives the rendered component (for reference-resolution asserts). @@ -142,9 +176,6 @@ public sealed class EventContractSpec where TComponent : IComponent /// Extra render setup the event needs to be reachable (child components, data, ...). public Action>? Arrange { get; init; } - /// Dynamic payload builder for references only known after render (child ids); wins over . - public Func, string>? ArgsJsonFactory { get; init; } - /// Like but receives the rendered cut (to reach arranged children). public Action, object>? AssertWithCut { get; init; } @@ -169,12 +200,17 @@ public sealed class ComponentContract where TComponent : IComponent public IReadOnlyList> Events => _events; public IReadOnlyList> Props => _props; - /// A void API method (async-only members): asserts identifier, arguments and type tags. + /// + /// A void API method (async-only members): asserts identifier, arguments and type tags. + /// / as on the twin overload below. + /// public ComponentContract Method( Func invoke, string jsName, object?[]? args = null, string[]? types = null, + Action>? arrange = null, + Func>? elements = null, [CallerFilePath] string atFile = "", [CallerLineNumber] int atLine = 0) { @@ -184,6 +220,8 @@ public ComponentContract Method( Invoke = async c => { await invoke(c); return null; }, ExpectedArgs = args ?? [], ExpectedTypes = types ?? [], + Arrange = arrange, + ExpectedElements = elements, Source = new SpecSource(atFile, atLine), }); return this; @@ -195,7 +233,9 @@ public ComponentContract Method( Func> invoke, string jsName, object?[]? args = null, - string[]? types = null) + string[]? types = null, + Action>? arrange = null, + Func>? elements = null) => throw new NotSupportedException(); /// @@ -209,22 +249,26 @@ public ComponentContract Method( TResult returns, object?[]? args = null, string[]? types = null, + Action>? arrange = null, + Func>? elements = null, [CallerFilePath] string atFile = "", [CallerLineNumber] int atLine = 0) - => Method(invoke, jsName, StubFor(returns), returns, args, types, atFile, atLine); + => Method(invoke, jsName, StubFor(returns), returns, args, types, arrange, elements, atFile, atLine); /// /// Value-returning method overload (async-only members) for wire returns the value /// form can't express (object/array envelopes, or stubs that decode to a different - /// value than sent). + /// value than sent, or a stub only known once rendered — see ). /// public ComponentContract Method( Func> invoke, string jsName, - InteropReturn returns, + FromRender returns, TResult expect, object?[]? args = null, string[]? types = null, + Action>? arrange = null, + Func>? elements = null, [CallerFilePath] string atFile = "", [CallerLineNumber] int atLine = 0) { @@ -234,6 +278,8 @@ public ComponentContract Method( Invoke = async c => await invoke(c), ExpectedArgs = args ?? [], ExpectedTypes = types ?? [], + Arrange = arrange, + ExpectedElements = elements, Stub = returns, ExpectedReturn = expect, HasExpectedReturn = true, @@ -297,7 +343,7 @@ public ComponentContract Method( Func> invoke, Func sync, string jsName, - InteropReturn returns, + FromRender returns, TResult expect, object?[]? args = null, string[]? types = null, @@ -380,7 +426,7 @@ public ComponentContract Getter( public ComponentContract Getter( Func> invoke, string propertyName, - InteropReturn returns, + FromRender returns, TResult expect, [CallerFilePath] string atFile = "", [CallerLineNumber] int atLine = 0) @@ -408,7 +454,7 @@ public ComponentContract Getter( Func> invoke, string propertyName, Action> arrange, - Func, InteropReturn> returns, + FromRender returns, Action, TResult> assert, [CallerFilePath] string atFile = "", [CallerLineNumber] int atLine = 0) @@ -418,8 +464,7 @@ public ComponentContract Getter( ReadsProperty = propertyName, Invoke = async c => await invoke(c), Arrange = arrange, - // For arranged specs the render scope IS the typed cut. - StubFactory = (h, scope) => returns(h, (IRenderedComponent)scope), + Stub = returns, AssertReturnWithCut = (scope, o) => assert((IRenderedComponent)scope, (TResult)o!), Source = new SpecSource(atFile, atLine), }); @@ -439,7 +484,7 @@ public ComponentContract Getter( string propertyName, Func> host, Func, IRenderedComponent> target, - Func, InteropReturn> returns, + FromRender returns, Action, TResult> assert, [CallerFilePath] string atFile = "", [CallerLineNumber] int atLine = 0) @@ -450,7 +495,7 @@ public ComponentContract Getter( Invoke = async c => await invoke(c), Host = host, Target = target, - StubFactory = returns, + Stub = returns, AssertReturnWithCut = (scope, o) => assert(scope, (TResult)o!), Source = new SpecSource(atFile, atLine), }); @@ -472,7 +517,7 @@ public ComponentContract Getter( Func> invoke, Func sync, string propertyName, - InteropReturn returns, + FromRender returns, TResult expect, [CallerFilePath] string atFile = "", [CallerLineNumber] int atLine = 0) @@ -496,7 +541,7 @@ public ComponentContract Getter( Func sync, string propertyName, Action> arrange, - Func, InteropReturn> returns, + FromRender returns, Action, TResult> assert, [CallerFilePath] string atFile = "", [CallerLineNumber] int atLine = 0) @@ -507,7 +552,7 @@ public ComponentContract Getter( Invoke = async c => await invoke(c), SyncInvoke = c => sync(c), Arrange = arrange, - StubFactory = (h, scope) => returns(h, (IRenderedComponent)scope), + Stub = returns, AssertReturnWithCut = (scope, o) => assert((IRenderedComponent)scope, (TResult)o!), Source = new SpecSource(atFile, atLine), }); @@ -521,7 +566,7 @@ public ComponentContract Getter( string propertyName, Func> host, Func, IRenderedComponent> target, - Func, InteropReturn> returns, + FromRender returns, Action, TResult> assert, [CallerFilePath] string atFile = "", [CallerLineNumber] int atLine = 0) @@ -533,7 +578,7 @@ public ComponentContract Getter( SyncInvoke = c => sync(c), Host = host, Target = target, - StubFactory = returns, + Stub = returns, AssertReturnWithCut = (scope, o) => assert(scope, (TResult)o!), Source = new SpecSource(atFile, atLine), }); @@ -556,12 +601,17 @@ public ComponentContract Prop( /// /// Prop overload stating the wire value explicitly — required for enums (wire enum /// value) and serialized objects/arrays (/). + /// adds render setup the transmission depends on (e.g. the + /// data items the value references); pass a late + /// () when it can only be known once that has rendered — + /// tracked items, for instance, cross as refs whose ids are assigned on transfer. /// public ComponentContract Prop( Expression> member, TValue value, - object? wire, + FromRender wire, string? wireName = null, + Action>? arrange = null, [CallerFilePath] string atFile = "", [CallerLineNumber] int atLine = 0) { @@ -570,31 +620,7 @@ public ComponentContract Prop( WireName = wireName ?? WirePropertyName(member), Set = ps => ps.Add(member, value), ExpectedValue = wire, - Source = new SpecSource(atFile, atLine), - }); - return this; - } - - /// - /// Prop overload with render arrangement and a dynamic wire value — for values whose - /// transmission references arranged state (e.g. data items crossing as uuid refs - /// whose ids are only assigned once the data source transfers). - /// - public ComponentContract Prop( - Expression> member, - TValue value, - Action> arrange, - Func, object?> wire, - string? wireName = null, - [CallerFilePath] string atFile = "", - [CallerLineNumber] int atLine = 0) - { - _props.Add(new StatePropContractSpec - { - WireName = wireName ?? WirePropertyName(member), - Set = ps => ps.Add(member, value), Arrange = arrange, - ExpectedValueFactory = wire, Source = new SpecSource(atFile, atLine), }); return this; @@ -677,14 +703,15 @@ public ComponentContract Event( /// /// Event overload for payloads referencing arranged children: - /// adds the children (or data) the event needs, builds the - /// payload after render (when child ids exist), and receives - /// the rendered cut so it can compare against the arranged child instances. + /// adds the children (or data) the event needs, a late + /// () builds the payload after render (when child ids + /// exist), and receives the rendered cut so it can compare + /// against the arranged child instances. /// public ComponentContract Event( Expression>> member, Action> arrange, - Func, string> argsJson, + FromRender argsJson, Action, TArgs> assert, [CallerFilePath] string atFile = "", [CallerLineNumber] int atLine = 0) @@ -696,7 +723,7 @@ public ComponentContract Event( Get = GetterOf(member), ArgsType = typeof(TArgs), Arrange = arrange, - ArgsJsonFactory = argsJson, + ArgsJson = argsJson, AssertWithCut = (cut, o) => assert(cut, (TArgs)o), Source = new SpecSource(atFile, atLine), }); diff --git a/tests/IgniteUI.Blazor.Tests/SelectTests.cs b/tests/IgniteUI.Blazor.Tests/SelectTests.cs index de1ff6f7..b80416d3 100644 --- a/tests/IgniteUI.Blazor.Tests/SelectTests.cs +++ b/tests/IgniteUI.Blazor.Tests/SelectTests.cs @@ -33,7 +33,7 @@ public class SelectTests : ComponentWithContractTestBase .Getter(c => c.GetCurrentValueAsync(), c => c.GetCurrentValue(), "Value", returns: "us") .Getter(c => c.GetItemsAsync(), c => c.GetItems(), "Items", arrangeItems, - returns: (interop, cut) => InteropReturn.Array($$$"""[{"refType": "name", "id": "{{{interop.ContainerIdOf(cut, "igc-select-item:nth-of-type(1)")}}}"}, {"refType": "name", "id": "{{{interop.ContainerIdOf(cut, "igc-select-item:nth-of-type(2)")}}}"}]"""), + returns: FromRender.Of((interop, cut) => InteropReturn.Array($$$"""[{"refType": "name", "id": "{{{interop.ContainerIdOf(cut, "igc-select-item:nth-of-type(1)")}}}"}, {"refType": "name", "id": "{{{interop.ContainerIdOf(cut, "igc-select-item:nth-of-type(2)")}}}"}]""")), assert: (cut, result) => { Assert.Equal(2, result.Length); @@ -42,7 +42,7 @@ public class SelectTests : ComponentWithContractTestBase }) .Getter(c => c.GetGroupsAsync(), c => c.GetGroups(), "Groups", arrangeGroups, - returns: (interop, cut) => InteropReturn.Array($$$"""[{"refType": "name", "id": "{{{interop.ContainerIdOf(cut, "igc-select-group:nth-of-type(1)")}}}"}]"""), + returns: FromRender.Of((interop, cut) => InteropReturn.Array($$$"""[{"refType": "name", "id": "{{{interop.ContainerIdOf(cut, "igc-select-group:nth-of-type(1)")}}}"}]""")), assert: (cut, result) => { Assert.Single(result); @@ -52,7 +52,7 @@ public class SelectTests : ComponentWithContractTestBase }) .Getter(c => c.GetSelectedItemAsync(), c => c.GetSelectedItem(), "SelectedItem", arrangeItems, - returns: (interop, cut) => InteropReturn.Ref($$"""{"refType": "name", "id": "{{interop.ContainerIdOf(cut, "igc-select-item:nth-of-type(1)")}}"}"""), + returns: FromRender.Of((interop, cut) => InteropReturn.Ref($$"""{"refType": "name", "id": "{{interop.ContainerIdOf(cut, "igc-select-item:nth-of-type(1)")}}"}""")), assert: (cut, result) => Assert.Same(cut.FindComponents()[0].Instance, result)) .Method(c => c.FocusComponentAsync(new IgbFocusOptions { PreventScroll = true }), c => c.FocusComponent(new IgbFocusOptions { PreventScroll = true }), "focus", args: [new JsonSubset("""{"preventScroll": true}""")], types: ["Json"]) @@ -70,7 +70,7 @@ public class SelectTests : ComponentWithContractTestBase .Event(c => c.Closed) .Event(c => c.Change, arrangeItems, - argsJson: (interop, cut) => $$$"""{"detail": {"refType": "name", "id": "{{{interop.ContainerIdOf(cut, "igc-select-item:nth-of-type(2)")}}}"}}""", + argsJson: FromRender.Of((interop, cut) => $$$"""{"detail": {"refType": "name", "id": "{{{interop.ContainerIdOf(cut, "igc-select-item:nth-of-type(2)")}}}"}}"""), assert: (cut, args) => { Assert.Same(cut.FindComponents()[1].Instance, args.Detail); diff --git a/tests/IgniteUI.Blazor.Tests/StepperTests.cs b/tests/IgniteUI.Blazor.Tests/StepperTests.cs index 9f2a99f5..558d5b37 100644 --- a/tests/IgniteUI.Blazor.Tests/StepperTests.cs +++ b/tests/IgniteUI.Blazor.Tests/StepperTests.cs @@ -25,7 +25,7 @@ public class StepperTests : ComponentWithContractTestBase .Method(c => c.ResetAsync(), c => c.Reset(), "reset") .Getter(c => c.GetStepsAsync(), c => c.GetSteps(), "Steps", arrange, - returns: (interop, cut) => InteropReturn.Array($$$"""[{"refType": "name", "id": "{{{interop.ContainerIdOf(cut, "igc-step:nth-of-type(1)")}}}"}, {"refType": "name", "id": "{{{interop.ContainerIdOf(cut, "igc-step:nth-of-type(2)")}}}"}]"""), + returns: FromRender.Of((interop, cut) => InteropReturn.Array($$$"""[{"refType": "name", "id": "{{{interop.ContainerIdOf(cut, "igc-step:nth-of-type(1)")}}}"}, {"refType": "name", "id": "{{{interop.ContainerIdOf(cut, "igc-step:nth-of-type(2)")}}}"}]""")), assert: (cut, result) => { Assert.Equal(2, result.Length); diff --git a/tests/IgniteUI.Blazor.Tests/TabsTests.cs b/tests/IgniteUI.Blazor.Tests/TabsTests.cs index f61ab468..c6fd9014 100644 --- a/tests/IgniteUI.Blazor.Tests/TabsTests.cs +++ b/tests/IgniteUI.Blazor.Tests/TabsTests.cs @@ -17,7 +17,7 @@ public class TabsTests : ComponentWithContractTestBase builder.AddAttribute(3, "id", "tab-2"); builder.CloseComponent(); }), - argsJson: (interop, cut) => $$$"""{"detail": {"refType": "name", "id": "{{{interop.ContainerIdOf(cut, "igc-tab:nth-of-type(2)")}}}"}}""", + argsJson: FromRender.Of((interop, cut) => $$$"""{"detail": {"refType": "name", "id": "{{{interop.ContainerIdOf(cut, "igc-tab:nth-of-type(2)")}}}"}}"""), assert: (cut, args) => { Assert.Same(cut.Instance.ActualTabsCollection[1], args.Detail); diff --git a/tests/IgniteUI.Blazor.Tests/TileManagerTests.cs b/tests/IgniteUI.Blazor.Tests/TileManagerTests.cs index 41280f5c..216e0dfa 100644 --- a/tests/IgniteUI.Blazor.Tests/TileManagerTests.cs +++ b/tests/IgniteUI.Blazor.Tests/TileManagerTests.cs @@ -24,7 +24,7 @@ public class TileManagerTests : ComponentWithContractTestBase args: ["{\"tiles\":[]}"], types: ["String"]) .Getter(c => c.GetTilesAsync(), c => c.GetTiles(), "Tiles", arrange, - returns: (interop, cut) => InteropReturn.Array($$$"""[{"refType": "name", "id": "{{{interop.ContainerIdOf(cut, "igc-tile:nth-of-type(1)")}}}"}, {"refType": "name", "id": "{{{interop.ContainerIdOf(cut, "igc-tile:nth-of-type(2)")}}}"}]"""), + returns: FromRender.Of((interop, cut) => InteropReturn.Array($$$"""[{"refType": "name", "id": "{{{interop.ContainerIdOf(cut, "igc-tile:nth-of-type(1)")}}}"}, {"refType": "name", "id": "{{{interop.ContainerIdOf(cut, "igc-tile:nth-of-type(2)")}}}"}]""")), assert: (cut, result) => { Assert.Equal(2, result.Length); @@ -33,31 +33,31 @@ public class TileManagerTests : ComponentWithContractTestBase }) .Event(c => c.TileDragStart, arrange, - argsJson: (interop, cut) => $$$"""{"detail": {"refType": "name", "id": "{{{interop.ContainerIdOf(cut, "igc-tile:nth-of-type(2)")}}}"}}""", + argsJson: FromRender.Of((interop, cut) => $$$"""{"detail": {"refType": "name", "id": "{{{interop.ContainerIdOf(cut, "igc-tile:nth-of-type(2)")}}}"}}"""), assert: (cut, args) => Assert.Same(cut.FindComponents()[1].Instance, args.Detail)) .Event(c => c.TileDragEnd, arrange, - argsJson: (interop, cut) => $$$"""{"detail": {"refType": "name", "id": "{{{interop.ContainerIdOf(cut, "igc-tile:nth-of-type(2)")}}}"}}""", + argsJson: FromRender.Of((interop, cut) => $$$"""{"detail": {"refType": "name", "id": "{{{interop.ContainerIdOf(cut, "igc-tile:nth-of-type(2)")}}}"}}"""), assert: (cut, args) => Assert.Same(cut.FindComponents()[1].Instance, args.Detail)) .Event(c => c.TileDragCancel, arrange, - argsJson: (interop, cut) => $$$"""{"detail": {"refType": "name", "id": "{{{interop.ContainerIdOf(cut, "igc-tile:nth-of-type(2)")}}}"}}""", + argsJson: FromRender.Of((interop, cut) => $$$"""{"detail": {"refType": "name", "id": "{{{interop.ContainerIdOf(cut, "igc-tile:nth-of-type(2)")}}}"}}"""), assert: (cut, args) => Assert.Same(cut.FindComponents()[1].Instance, args.Detail)) .Event(c => c.TileResizeStart, arrange, - argsJson: (interop, cut) => $$$"""{"detail": {"refType": "name", "id": "{{{interop.ContainerIdOf(cut, "igc-tile:nth-of-type(2)")}}}"}}""", + argsJson: FromRender.Of((interop, cut) => $$$"""{"detail": {"refType": "name", "id": "{{{interop.ContainerIdOf(cut, "igc-tile:nth-of-type(2)")}}}"}}"""), assert: (cut, args) => Assert.Same(cut.FindComponents()[1].Instance, args.Detail)) .Event(c => c.TileResizeEnd, arrange, - argsJson: (interop, cut) => $$$"""{"detail": {"refType": "name", "id": "{{{interop.ContainerIdOf(cut, "igc-tile:nth-of-type(2)")}}}"}}""", + argsJson: FromRender.Of((interop, cut) => $$$"""{"detail": {"refType": "name", "id": "{{{interop.ContainerIdOf(cut, "igc-tile:nth-of-type(2)")}}}"}}"""), assert: (cut, args) => Assert.Same(cut.FindComponents()[1].Instance, args.Detail)) .Event(c => c.TileResizeCancel, arrange, - argsJson: (interop, cut) => $$$"""{"detail": {"refType": "name", "id": "{{{interop.ContainerIdOf(cut, "igc-tile:nth-of-type(2)")}}}"}}""", + argsJson: FromRender.Of((interop, cut) => $$$"""{"detail": {"refType": "name", "id": "{{{interop.ContainerIdOf(cut, "igc-tile:nth-of-type(2)")}}}"}}"""), assert: (cut, args) => Assert.Same(cut.FindComponents()[1].Instance, args.Detail)) .Event(c => c.TileFullscreen, arrange, - argsJson: (interop, cut) => $$$$"""{"detail": {"retType": "object", "type": "", "value": {"tile": {"refType": "name", "id": "{{{{interop.ContainerIdOf(cut, "igc-tile:nth-of-type(2)")}}}}"}, "state": true}}}""", + argsJson: FromRender.Of((interop, cut) => $$$$"""{"detail": {"retType": "object", "type": "", "value": {"tile": {"refType": "name", "id": "{{{{interop.ContainerIdOf(cut, "igc-tile:nth-of-type(2)")}}}}"}, "state": true}}}"""), assert: (cut, args) => { Assert.Same(cut.FindComponents()[1].Instance, args.Detail.Tile); @@ -65,7 +65,7 @@ public class TileManagerTests : ComponentWithContractTestBase }) .Event(c => c.TileMaximize, arrange, - argsJson: (interop, cut) => $$$$"""{"detail": {"retType": "object", "type": "", "value": {"tile": {"refType": "name", "id": "{{{{interop.ContainerIdOf(cut, "igc-tile:nth-of-type(2)")}}}}"}, "state": false}}}""", + argsJson: FromRender.Of((interop, cut) => $$$$"""{"detail": {"retType": "object", "type": "", "value": {"tile": {"refType": "name", "id": "{{{{interop.ContainerIdOf(cut, "igc-tile:nth-of-type(2)")}}}}"}, "state": false}}}"""), assert: (cut, args) => { Assert.Same(cut.FindComponents()[1].Instance, args.Detail.Tile); diff --git a/tests/IgniteUI.Blazor.Tests/TreeTests.cs b/tests/IgniteUI.Blazor.Tests/TreeTests.cs index 91455365..8411fb25 100644 --- a/tests/IgniteUI.Blazor.Tests/TreeTests.cs +++ b/tests/IgniteUI.Blazor.Tests/TreeTests.cs @@ -22,27 +22,27 @@ public class TreeTests : ComponentWithContractTestBase protected override ComponentContract InteropContract { get; } = new ComponentContract() .Event(c => c.ItemExpanding, arrange, - argsJson: (interop, cut) => $$$"""{"detail": {"refType": "name", "id": "{{{interop.ContainerIdOf(cut, "igc-tree-item:nth-of-type(2)")}}}"}}""", + argsJson: FromRender.Of((interop, cut) => $$$"""{"detail": {"refType": "name", "id": "{{{interop.ContainerIdOf(cut, "igc-tree-item:nth-of-type(2)")}}}"}}"""), assert: (cut, args) => Assert.Same(cut.Instance.ContentItems[1], args.Detail)) .Event(c => c.ItemExpanded, arrange, - argsJson: (interop, cut) => $$$"""{"detail": {"refType": "name", "id": "{{{interop.ContainerIdOf(cut, "igc-tree-item:nth-of-type(2)")}}}"}}""", + argsJson: FromRender.Of((interop, cut) => $$$"""{"detail": {"refType": "name", "id": "{{{interop.ContainerIdOf(cut, "igc-tree-item:nth-of-type(2)")}}}"}}"""), assert: (cut, args) => Assert.Same(cut.Instance.ContentItems[1], args.Detail)) .Event(c => c.ItemCollapsing, arrange, - argsJson: (interop, cut) => $$$"""{"detail": {"refType": "name", "id": "{{{interop.ContainerIdOf(cut, "igc-tree-item:nth-of-type(2)")}}}"}}""", + argsJson: FromRender.Of((interop, cut) => $$$"""{"detail": {"refType": "name", "id": "{{{interop.ContainerIdOf(cut, "igc-tree-item:nth-of-type(2)")}}}"}}"""), assert: (cut, args) => Assert.Same(cut.Instance.ContentItems[1], args.Detail)) .Event(c => c.ItemCollapsed, arrange, - argsJson: (interop, cut) => $$$"""{"detail": {"refType": "name", "id": "{{{interop.ContainerIdOf(cut, "igc-tree-item:nth-of-type(2)")}}}"}}""", + argsJson: FromRender.Of((interop, cut) => $$$"""{"detail": {"refType": "name", "id": "{{{interop.ContainerIdOf(cut, "igc-tree-item:nth-of-type(2)")}}}"}}"""), assert: (cut, args) => Assert.Same(cut.Instance.ContentItems[1], args.Detail)) .Event(c => c.ActiveItem, arrange, - argsJson: (interop, cut) => $$$"""{"detail": {"refType": "name", "id": "{{{interop.ContainerIdOf(cut, "igc-tree-item:nth-of-type(2)")}}}"}}""", + argsJson: FromRender.Of((interop, cut) => $$$"""{"detail": {"refType": "name", "id": "{{{interop.ContainerIdOf(cut, "igc-tree-item:nth-of-type(2)")}}}"}}"""), assert: (cut, args) => Assert.Same(cut.Instance.ContentItems[1], args.Detail)) .Event(c => c.SelectionChanged, arrange, - argsJson: (interop, cut) => $$$$$"""{"detail": {"retType": "object", "type": "", "value": {"newSelection": {"retType": "Array", "type": "", "value": [{"refType": "name", "id": "{{{{{interop.ContainerIdOf(cut, "igc-tree-item:nth-of-type(2)")}}}}}"}]}}}}""", + argsJson: FromRender.Of((interop, cut) => $$$$$"""{"detail": {"retType": "object", "type": "", "value": {"newSelection": {"retType": "Array", "type": "", "value": [{"refType": "name", "id": "{{{{{interop.ContainerIdOf(cut, "igc-tree-item:nth-of-type(2)")}}}}}"}]}}}}"""), assert: (cut, args) => Assert.Same(cut.Instance.ContentItems[1], args.Detail.NewSelection[0])); [Fact] @@ -179,7 +179,7 @@ public class TreeItemTests : ComponentWithContractTestBase .Method(c => c.CollapseAsync(), c => c.Collapse(), "collapse") .Getter(c => c.GetPathAsync(), c => c.GetPath(), "Path", arrange: ps => { }, - returns: (interop, cut) => InteropReturn.Array("""[{"refType": "name", "id": "mainControl"}]"""), + returns: FromRender.Of((interop, cut) => InteropReturn.Array("""[{"refType": "name", "id": "mainControl"}]""")), assert: (cut, result) => { Assert.Single(result); @@ -188,7 +188,7 @@ public class TreeItemTests : ComponentWithContractTestBase .Getter(c => c.GetPathAsync(), c => c.GetPath(), "Path", host: treeHost, target: h => h.FindComponents()[1], // Child 1.1 - returns: (interop, h) => InteropReturn.Array($$$"""[{"refType": "name", "id": "{{{interop.ContainerIdOf(h, "igc-tree-item")}}}"}, {"refType": "name", "id": "mainControl"}]"""), + returns: FromRender.Of((interop, h) => InteropReturn.Array($$$"""[{"refType": "name", "id": "{{{interop.ContainerIdOf(h, "igc-tree-item")}}}"}, {"refType": "name", "id": "mainControl"}]""")), assert: (h, result) => { Assert.Equal(2, result.Length);