diff --git a/CHANGELOG.md b/CHANGELOG.md index e81bae127..e5c427312 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,9 +9,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) ### Added List of new features. +- Added `InvokeAsync(Func)` to `RenderedComponentInvokeAsyncExtensions`. By [@JeroenBos](https://github.com/JeroenBos) in [#151](https://github.com/egil/bUnit/pull/177). +- Added `ITestRenderer Renderer { get ; }` to `IRenderedFragment` to make it possible to simplify the `IRenderedComponentBase` interface. By [@JeroenBos](https://github.com/JeroenBos) in [#151](https://github.com/egil/bUnit/pull/177). + ### Changed List of changes in existing functionality. +- Moved `InvokeAsync()`, `Render()` and `SetParametersAndRender()` methods out of `IRenderedComponentBase` into extension methods. By [@JeroenBos](https://github.com/JeroenBos) in [#151](https://github.com/egil/bUnit/pull/177). + ### Deprecated List of soon-to-be removed features. diff --git a/docs/site/docs/interaction/trigger-renders.md b/docs/site/docs/interaction/trigger-renders.md index e19d7e54a..f2b2a9d11 100644 --- a/docs/site/docs/interaction/trigger-renders.md +++ b/docs/site/docs/interaction/trigger-renders.md @@ -5,7 +5,7 @@ title: Triggering a Render Life Cycle on a Component # Triggering a Render Life Cycle on a Component -When a component under test is rendered, an instance of the type is returned. Through that, it is possible to cause the component under test to render again directly through the method or one of the [`SetParametersAndRender(...)`](xref:Bunit.IRenderedComponentBase`1.SetParametersAndRender(Bunit.Rendering.ComponentParameter[])) methods or indirectly through the method. +When a component under test is rendered, an instance of the type is returned. Through that, it is possible to cause the component under test to render again directly through the [`Render()`](xref:Bunit.RenderedComponentRenderExtensions.Render``1(Bunit.IRenderedComponentBase{``0})) method or one of the [`SetParametersAndRender(...)`](xref:Bunit.RenderedComponentRenderExtensions.SetParametersAndRender``1(Bunit.IRenderedComponentBase{``0},System.Action{Bunit.ComponentParameterBuilder{``0}})) methods or indirectly through the [`InvokeAsync(...)`](xref:Bunit.RenderedComponentInvokeAsyncExtensions.InvokeAsync``1(Bunit.IRenderedComponentBase{``0},System.Action)) method. > [!WARNING] > The `Render()` and `SetParametersAndRender()` methods are not available in the type that is returned when calling the _non_-generic version of `GetComponentUnderTest()` in ``-based Razor tests. Call the generic version of `GetComponentUnderTest()` to get a . @@ -17,25 +17,25 @@ Let's look at how to use each of these methods to cause a re-render. ## Render -The tells the renderer to re-render the component, i.e. go through its life-cycle methods (except for `OnInitialized()` and `OnInitializedAsync()` methods). To use it, do the following: +The [`Render()`](xref:Bunit.RenderedComponentRenderExtensions.Render``1(Bunit.IRenderedComponentBase{``0})) tells the renderer to re-render the component, i.e. go through its life-cycle methods (except for `OnInitialized()` and `OnInitializedAsync()` methods). To use it, do the following: [!code-csharp[](../../../samples/tests/xunit/ReRenderTest.cs?start=17&end=24&highlight=6)] -The highlighted line shows the call to . +The highlighted line shows the call to [`Render()`](xref:Bunit.RenderedComponentRenderExtensions.Render``1(Bunit.IRenderedComponentBase{``0})). > [!TIP] > The number of renders a component has been through can be inspected and verified using the property. ## SetParametersAndRender -The [`SetParametersAndRender(...)`](xref:Bunit.IRenderedComponentBase`1.SetParametersAndRender(Bunit.Rendering.ComponentParameter[])) methods tells the renderer to re-render the component with new parameters, i.e. go through its life-cycle methods (except for `OnInitialized()` and `OnInitializedAsync()` methods), passing the new parameters to the `SetParametersAsync()` method, _but only the new parameters_. To use it, do the following: +The [`SetParametersAndRender(...)`](xref:Bunit.RenderedComponentRenderExtensions.SetParametersAndRender``1(Bunit.IRenderedComponentBase{``0},System.Action{Bunit.ComponentParameterBuilder{``0}})) methods tells the renderer to re-render the component with new parameters, i.e. go through its life-cycle methods (except for `OnInitialized()` and `OnInitializedAsync()` methods), passing the new parameters to the `SetParametersAsync()` method, _but only the new parameters_. To use it, do the following: [!code-csharp[](../../../samples/tests/xunit/ReRenderTest.cs?start=31&end=42&highlight=8-10)] -The highlighted line shows the call to , which is also available as if you prefer that method of passing parameters. +The highlighted line shows the call to [`SetParametersAndRender(parameter builder)`](xref:Bunit.RenderedComponentRenderExtensions.SetParametersAndRender``1(Bunit.IRenderedComponentBase{``0},System.Action{Bunit.ComponentParameterBuilder{``0}})), which is also available as a version that takes the zero or more component parameters, e.g. created through the component parameter factory helper methods, if you prefer that method of passing parameters. > [!NOTE] -> Passing parameters to components through the [`SetParametersAndRender(...)`](xref:Bunit.IRenderedComponentBase`1.SetParametersAndRender(Bunit.Rendering.ComponentParameter[])) methods is identical to doing it with the [`RenderComponent(...)`](xref:Bunit.IRenderedComponentBase`1.SetParametersAndRender(Bunit.Rendering.ComponentParameter[])) methods, described in detail on the page. +> Passing parameters to components through the [`SetParametersAndRender(...)`](xref:Bunit.RenderedComponentRenderExtensions.SetParametersAndRender``1(Bunit.IRenderedComponentBase{``0},System.Action{Bunit.ComponentParameterBuilder{``0}})) methods is identical to doing it with the `RenderComponent(...)` methods, described in detail on the page. ## InvokeAsync @@ -43,7 +43,7 @@ Invoking methods on a component under test, which causes a render, e.g. by calli > The current thread is not associated with the Dispatcher. Use InvokeAsync() to switch execution to the Dispatcher when triggering rendering or component state. -If you receive this error, you need to invoke your method inside an `Action` delegate passed to the method. +If you receive this error, you need to invoke your method inside an `Action` delegate passed to the [`InvokeAsync(...)`](xref:Bunit.RenderedComponentInvokeAsyncExtensions.InvokeAsync``1(Bunit.IRenderedComponentBase{``0},System.Action)) method. Consider the `` component listed below: @@ -53,7 +53,7 @@ To invoke the `Calculate()` method on the component instance, do the following: [!code-csharp[](../../../samples/tests/xunit/ReRenderTest.cs?start=49&end=56&highlight=6)] -The highlighted line shows the call to , which is passed an `Action` delegate, that calls the `Calculate` method. +The highlighted line shows the call to [`InvokeAsync(...)`](xref:Bunit.RenderedComponentInvokeAsyncExtensions.InvokeAsync``1(Bunit.IRenderedComponentBase{``0},System.Action)), which is passed an `Action` delegate, that calls the `Calculate` method. > [!TIP] > The instance of a component under test is available through the property. \ No newline at end of file diff --git a/src/bunit.core.tests/ComponentParameterFactoryTest.cs b/src/bunit.core.tests/ComponentParameterFactoryTest.cs index c57dc1682..9a8e424b0 100644 --- a/src/bunit.core.tests/ComponentParameterFactoryTest.cs +++ b/src/bunit.core.tests/ComponentParameterFactoryTest.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Threading.Tasks; using Bunit.Rendering; using Bunit.TestAssets.SampleComponents; using Bunit.TestDoubles.JSInterop; diff --git a/src/bunit.core.tests/Extensions/RenderedComponentInvokeAsyncExtensionsTest.cs b/src/bunit.core.tests/Extensions/RenderedComponentInvokeAsyncExtensionsTest.cs new file mode 100644 index 000000000..323a8e2fb --- /dev/null +++ b/src/bunit.core.tests/Extensions/RenderedComponentInvokeAsyncExtensionsTest.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Bunit.TestAssets.SampleComponents; +using Shouldly; +using Xunit; + +namespace Bunit.Extensions +{ + public class RenderedComponentInvokeAsyncExtensionsTest : TestContext + { + [Fact(DisplayName = "Dispatcher awaits Task-returning callback")] + public async Task Test003() + { + // Arrange + var cut = RenderComponent(); + bool delegateFinished = false; + + async Task Callback() + { + await Task.Delay(10); + delegateFinished = true; + } + + // Act + await cut.InvokeAsync(Callback); + + // Assert + delegateFinished.ShouldBeTrue(); + } + + [Fact(DisplayName = "Dispatcher does not await void-returning callback")] + public async Task Test004() + { + // Arrange + var cut = RenderComponent(); + bool delegateFinished = false; + + async void Callback() + { + await Task.Delay(10); + delegateFinished = true; + } + + // Act + await cut.InvokeAsync(Callback); + + // Assert + delegateFinished.ShouldBeFalse(); + } + } +} diff --git a/src/bunit.core/Extensions/RenderedComponentInvokeAsyncExtension.cs b/src/bunit.core/Extensions/RenderedComponentInvokeAsyncExtension.cs new file mode 100644 index 000000000..cf444aa99 --- /dev/null +++ b/src/bunit.core/Extensions/RenderedComponentInvokeAsyncExtension.cs @@ -0,0 +1,44 @@ +using System; +using System.Text; +using System.Threading.Tasks; +using Bunit.Rendering; +using Microsoft.AspNetCore.Components; + +namespace Bunit +{ + /// + /// InvokeAsync extensions methods on . + /// + public static class RenderedComponentInvokeAsyncExtensions + { + /// + /// Invokes the given in the context of the associated . + /// + /// The rendered component whose dispatcher to invoke with. + /// + /// A that will be completed when the action has finished executing or is suspended by an asynchronous operation. + public static Task InvokeAsync(this IRenderedComponentBase renderedComponent, Action callback) + where TComponent : IComponent + { + if (renderedComponent is null) + throw new ArgumentNullException(nameof(renderedComponent)); + + return renderedComponent.Renderer.Dispatcher.InvokeAsync(callback); + } + + /// + /// Invokes the given in the context of the associated . + /// + /// The rendered component whose dispatcher to invoke with. + /// + /// A that will be completed when the action has finished executing. + public static Task InvokeAsync(this IRenderedComponentBase renderedComponent, Func callback) + where TComponent : IComponent + { + if (renderedComponent is null) + throw new ArgumentNullException(nameof(renderedComponent)); + + return renderedComponent.Renderer.Dispatcher.InvokeAsync(callback); + } + } +} diff --git a/src/bunit.core/Extensions/RenderedComponentRenderExtensions.cs b/src/bunit.core/Extensions/RenderedComponentRenderExtensions.cs new file mode 100644 index 000000000..b2f158c44 --- /dev/null +++ b/src/bunit.core/Extensions/RenderedComponentRenderExtensions.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Bunit.Rendering; +using Microsoft.AspNetCore.Components; + +namespace Bunit +{ + /// + /// Re-render extension methods, optionally with new parameters, for . + /// + public static class RenderedComponentRenderExtensions + { + /// + /// Render the component under test again. + /// + /// The rendered component to re-render. + public static void Render(this IRenderedComponentBase renderedComponent) where TComponent : IComponent + => SetParametersAndRender(renderedComponent, ParameterView.Empty); + + /// + /// Render the component under test again with the provided . + /// + /// The rendered component to re-render with new parameters + /// Parameters to pass to the component upon rendered + public static void SetParametersAndRender(this IRenderedComponentBase renderedComponent, ParameterView parameters) + where TComponent : IComponent + { + if (renderedComponent is null) + throw new ArgumentNullException(nameof(renderedComponent)); + + renderedComponent.InvokeAsync(() => + { + renderedComponent.Instance.SetParametersAsync(parameters); + }); + } + + /// + /// Render the component under test again with the provided . + /// + /// The rendered component to re-render with new parameters + /// Parameters to pass to the component upon rendered + public static void SetParametersAndRender(this IRenderedComponentBase renderedComponent, params ComponentParameter[] parameters) where TComponent : IComponent + { + if (renderedComponent is null) + throw new ArgumentNullException(nameof(renderedComponent)); + SetParametersAndRender(renderedComponent, ToParameterView(parameters)); + } + + /// + /// Render the component under test again with the provided parameters from the . + /// + /// The rendered component to re-render with new parameters + /// An action that receives a . + public static void SetParametersAndRender(this IRenderedComponentBase renderedComponent, Action> parameterBuilder) + where TComponent : IComponent + { + if (renderedComponent is null) + throw new ArgumentNullException(nameof(renderedComponent)); + if (parameterBuilder is null) + throw new ArgumentNullException(nameof(parameterBuilder)); + + var builder = new ComponentParameterBuilder(); + parameterBuilder(builder); + + SetParametersAndRender(renderedComponent, ToParameterView(builder.Build())); + } + + + private static ParameterView ToParameterView(IReadOnlyList parameters) + { + var parameterView = ParameterView.Empty; + if (parameters.Any()) + { + var paramDict = new Dictionary(); + foreach (var param in parameters) + { + if (param.IsCascadingValue) + throw new InvalidOperationException($"You cannot provide a new cascading value through the {nameof(SetParametersAndRender)} method."); + paramDict.Add(param.Name!, param.Value); + } + parameterView = ParameterView.FromDictionary(paramDict); + } + return parameterView; + } + } +} diff --git a/src/bunit.core/IRenderedComponentBase.cs b/src/bunit.core/IRenderedComponentBase.cs index 8b0ff0490..e6dc7f1bb 100644 --- a/src/bunit.core/IRenderedComponentBase.cs +++ b/src/bunit.core/IRenderedComponentBase.cs @@ -16,35 +16,5 @@ public interface IRenderedComponentBase : IRenderedFragmentBase wher /// Gets the component under test /// TComponent Instance { get; } - - /// - /// Invokes the given in the context of the associated . - /// - /// - /// A that will be completed when the action has finished executing. - Task InvokeAsync(Action callback); - - /// - /// Render the component under test again. - /// - void Render(); - - /// - /// Render the component under test again with the provided . - /// - /// Parameters to pass to the component upon rendered - void SetParametersAndRender(ParameterView parameters); - - /// - /// Render the component under test again with the provided . - /// - /// Parameters to pass to the component upon rendered - void SetParametersAndRender(params ComponentParameter[] parameters); - - /// - /// Render the component under test again with the provided parameters from the . - /// - /// An action that receives a . - void SetParametersAndRender(Action> parameterBuilder); } } diff --git a/src/bunit.core/IRenderedFragmentBase.cs b/src/bunit.core/IRenderedFragmentBase.cs index 059dd4bfa..6029f4946 100644 --- a/src/bunit.core/IRenderedFragmentBase.cs +++ b/src/bunit.core/IRenderedFragmentBase.cs @@ -27,6 +27,11 @@ public interface IRenderedFragmentBase /// /// Gets the used when rendering the component. /// - IServiceProvider Services { get; } + IServiceProvider Services { get; } + + /// + /// Gets the renderer that rendered the component. + /// + ITestRenderer Renderer { get; } } } diff --git a/src/bunit.web/Rendering/RenderedComponent.cs b/src/bunit.web/Rendering/RenderedComponent.cs index 9ff1e4ea5..7837a8cbf 100644 --- a/src/bunit.web/Rendering/RenderedComponent.cs +++ b/src/bunit.web/Rendering/RenderedComponent.cs @@ -15,56 +15,6 @@ internal class RenderedComponent : RenderedFragment, IRenderedCompon public RenderedComponent(IServiceProvider services, int componentId, TComponent component) : base(services, componentId) { Instance = component; - } - - /// - public Task InvokeAsync(Action callback) => Renderer.Dispatcher.InvokeAsync(callback); - - /// - public void Render() => SetParametersAndRender(ParameterView.Empty); - - /// - public void SetParametersAndRender(ParameterView parameters) - { - InvokeAsync(() => - { - Instance.SetParametersAsync(parameters); - }); - } - - /// - public void SetParametersAndRender(params ComponentParameter[] parameters) - { - SetParametersAndRender(ToParameterView(parameters)); - } - - /// - public void SetParametersAndRender(Action> parameterBuilder) - { - if (parameterBuilder is null) - throw new ArgumentNullException(nameof(parameterBuilder)); - - var builder = new ComponentParameterBuilder(); - parameterBuilder(builder); - - SetParametersAndRender(ToParameterView(builder.Build())); - } - - private static ParameterView ToParameterView(IReadOnlyList parameters) - { - var parameterView = ParameterView.Empty; - if (parameters.Any()) - { - var paramDict = new Dictionary(); - foreach (var param in parameters) - { - if (param.IsCascadingValue) - throw new InvalidOperationException($"You cannot provide a new cascading value through the {nameof(SetParametersAndRender)} method."); - paramDict.Add(param.Name!, param.Value); - } - parameterView = ParameterView.FromDictionary(paramDict); - } - return parameterView; - } + } } } diff --git a/src/bunit.web/Rendering/RenderedFragment.cs b/src/bunit.web/Rendering/RenderedFragment.cs index 558992014..cbddec5c4 100644 --- a/src/bunit.web/Rendering/RenderedFragment.cs +++ b/src/bunit.web/Rendering/RenderedFragment.cs @@ -29,16 +29,14 @@ public class RenderedFragment : IRenderedFragment, IRenderEventHandler private HtmlParser HtmlParser { get; } - /// - /// Gets the renderer used to render the . - /// - protected ITestRenderer Renderer { get; } - /// /// Gets the first rendered markup. /// protected string FirstRenderMarkup { get; } + /// + public ITestRenderer Renderer { get; } + /// public IServiceProvider Services { get; }