Skip to content

Commit 08ea910

Browse files
authored
feat: Add overloads to InvokeAsync with a return value (#1305)
* Add overloads to InvokeAsync with a return value Closes #1296 * Add more docs for InvokeAsync, fix line numbers in code references * Update wording
1 parent 61b3ff0 commit 08ea910

6 files changed

Lines changed: 114 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ All notable changes to **bUnit** will be documented in this file. The project ad
1414

1515
- When the `TestContext` was disposed, the Blazor Renderer itself didn't dispose components under test. By [@linkdotnet](https://github.com/linkdotnet).
1616

17+
### Added
18+
19+
- New overloads for `IRenderedFragmentBase.InvokeAsync` that allow retrieving the work item's return value. By [@jcparkyn](https://github.com/jcparkyn).
20+
1721
## [1.25.3] - 2023-11-14
1822

1923
- Upgrade all .NET 8 preview dependencies to .NET 8 stable.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<output>@(result is null ? "Loading" : result.ToString())</output>
2+
3+
@code
4+
{
5+
int? result = 0;
6+
7+
public async Task Calculate(int x, int y)
8+
{
9+
result = null;
10+
StateHasChanged();
11+
// Simulate an asynchronous operation, like fetching data.
12+
await Task.Delay(500);
13+
result = x + y;
14+
StateHasChanged();
15+
}
16+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<output>@result</output>
2+
3+
@code
4+
{
5+
int result = 0;
6+
7+
public int Calculate(int x, int y)
8+
{
9+
result = x + y;
10+
StateHasChanged();
11+
return result;
12+
}
13+
}

docs/samples/tests/xunit/ReRenderTest.cs

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public void RenderAgainUsingRender()
2525
[Fact]
2626
public void RenderAgainUsingSetParametersAndRender()
2727
{
28-
// Arrange - renders the Heading component
28+
// Arrange - renders the Item component
2929
var cut = RenderComponent<Item>(parameters => parameters
3030
.Add(p => p.Value, "Foo")
3131
);
@@ -42,7 +42,7 @@ public void RenderAgainUsingSetParametersAndRender()
4242
[Fact]
4343
public void RendersViaInvokeAsync()
4444
{
45-
// Arrange - renders the Heading component
45+
// Arrange - renders the Calc component
4646
var cut = RenderComponent<Calc>();
4747

4848
// Indirectly re-renders through the call to StateHasChanged
@@ -51,4 +51,34 @@ public void RendersViaInvokeAsync()
5151

5252
cut.MarkupMatches("<output>3</output>");
5353
}
54+
55+
[Fact]
56+
public async Task RendersViaInvokeAsyncWithReturnValue()
57+
{
58+
// Arrange - renders the CalcWithReturnValue component
59+
var cut = RenderComponent<CalcWithReturnValue>();
60+
61+
// Indirectly re-renders and returns a value.
62+
var result = await cut.InvokeAsync(() => cut.Instance.Calculate(1, 2));
63+
64+
Assert.Equal(3, result);
65+
cut.MarkupMatches("<output>3</output>");
66+
}
67+
68+
[Fact]
69+
public async Task RendersViaInvokeAsyncWithLoading()
70+
{
71+
// Arrange - renders the CalcWithLoading component
72+
var cut = RenderComponent<CalcWithLoading>();
73+
74+
// Indirectly re-renders and returns the task returned by Calculate().
75+
// The explicit <Task> here is important, otherwise the call to Calculate()
76+
// will be awaited automatically.
77+
var task = await cut.InvokeAsync<Task>(() => cut.Instance.Calculate(1, 2));
78+
cut.MarkupMatches("<output>Loading</output>");
79+
80+
// Wait for the task to complete.
81+
await task;
82+
cut.WaitForAssertion(() => cut.MarkupMatches("<output>3</output>"));
83+
}
5484
}

docs/site/docs/interaction/trigger-renders.md

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ Let's look at how to use each of these methods to cause a re-render.
1919

2020
The [`Render()`](xref:Bunit.RenderedComponentRenderExtensions.Render``1(Bunit.IRenderedComponentBase{``0})) method 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:
2121

22-
[!code-csharp[](../../../samples/tests/xunit/ReRenderTest.cs?start=17&end=23&highlight=5)]
22+
[!code-csharp[](../../../samples/tests/xunit/ReRenderTest.cs?start=16&end=22&highlight=5)]
2323

2424
The highlighted line shows the call to [`Render()`](xref:Bunit.RenderedComponentRenderExtensions.Render``1(Bunit.IRenderedComponentBase{``0})).
2525

@@ -30,7 +30,7 @@ The highlighted line shows the call to [`Render()`](xref:Bunit.RenderedComponent
3030

3131
The [`SetParametersAndRender(...)`](xref:Bunit.RenderedComponentRenderExtensions.SetParametersAndRender``1(Bunit.IRenderedComponentBase{``0},System.Action{Bunit.ComponentParameterCollectionBuilder{``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 &mdash; _but only the new parameters_ &mdash; to the `SetParametersAsync()` method. To use it, do the following:
3232

33-
[!code-csharp[](../../../samples/tests/xunit/ReRenderTest.cs?start=30&end=40&highlight=7-9)]
33+
[!code-csharp[](../../../samples/tests/xunit/ReRenderTest.cs?start=29&end=39&highlight=7-9)]
3434

3535
The highlighted line shows the call to [`SetParametersAndRender()`](xref:Bunit.RenderedComponentRenderExtensions.SetParametersAndRender``1(Bunit.IRenderedComponentBase{``0},System.Action{Bunit.ComponentParameterCollectionBuilder{``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.
3636

@@ -51,9 +51,25 @@ Let’s look at an example of this, using the `<Calc>` component listed below:
5151

5252
To invoke the `Calculate()` method on the component instance, do the following:
5353

54-
[!code-csharp[](../../../samples/tests/xunit/ReRenderTest.cs?start=47&end=53&highlight=5)]
54+
[!code-csharp[](../../../samples/tests/xunit/ReRenderTest.cs?start=46&end=52&highlight=5)]
5555

5656
The highlighted line shows the call to `InvokeAsync()`, which is passed an `Action` delegate that calls the `Calculate` method.
5757

5858
> [!TIP]
5959
> The instance of a component under test is available through the <xref:Bunit.IRenderedComponentBase`1.Instance> property.
60+
61+
### Advanced use cases
62+
63+
In some scenarios, the method being invoked may also return a value, as demonstrated in the following example.
64+
65+
[!code-cshtml[CalcWithReturnValue.razor](../../../samples/components/CalcWithReturnValue.razor)]
66+
67+
Testing this scenario follows the same procedure as before, with the addition of using the return value from `InvokeAsync()`:
68+
69+
[!code-csharp[](../../../samples/tests/xunit/ReRenderTest.cs?start=59&end=65&highlight=4)]
70+
71+
This can also be used to assert intermediate states during an asynchronous operation, like the example below:
72+
73+
[!code-cshtml[CalcWithLoading.razor](../../../samples/components/CalcWithLoading.razor)]
74+
75+
[!code-csharp[](../../../samples/tests/xunit/ReRenderTest.cs?start=71&end=82&highlight=7)]

src/bunit.core/Extensions/RenderedFragmentInvokeAsyncExtensions.cs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,34 @@ public static Task InvokeAsync(this IRenderedFragmentBase renderedFragment, Func
3636
return renderedFragment.Services.GetRequiredService<ITestRenderer>()
3737
.Dispatcher.InvokeAsync(workItem);
3838
}
39+
40+
/// <summary>
41+
/// Invokes the given <paramref name="workItem"/> in the context of the associated <see cref="ITestRenderer"/>.
42+
/// </summary>
43+
/// <param name="renderedFragment">The rendered component whose dispatcher to invoke with.</param>
44+
/// <param name="workItem">The work item to execute on the renderer's thread.</param>
45+
/// <returns>A <see cref="Task"/> that will be completed when the action has finished executing, with the return value from <paramref name="workItem"/>.</returns>
46+
public static Task<T> InvokeAsync<T>(this IRenderedFragmentBase renderedFragment, Func<T> workItem)
47+
{
48+
if (renderedFragment is null)
49+
throw new ArgumentNullException(nameof(renderedFragment));
50+
51+
return renderedFragment.Services.GetRequiredService<ITestRenderer>()
52+
.Dispatcher.InvokeAsync(workItem);
53+
}
54+
55+
/// <summary>
56+
/// Invokes the given <paramref name="workItem"/> in the context of the associated <see cref="ITestRenderer"/>.
57+
/// </summary>
58+
/// <param name="renderedFragment">The rendered component whose dispatcher to invoke with.</param>
59+
/// <param name="workItem">The work item to execute on the renderer's thread.</param>
60+
/// <returns>A <see cref="Task"/> that will be completed when the action has finished executing, with the return value from <paramref name="workItem"/>.</returns>
61+
public static Task<T> InvokeAsync<T>(this IRenderedFragmentBase renderedFragment, Func<Task<T>> workItem)
62+
{
63+
if (renderedFragment is null)
64+
throw new ArgumentNullException(nameof(renderedFragment));
65+
66+
return renderedFragment.Services.GetRequiredService<ITestRenderer>()
67+
.Dispatcher.InvokeAsync(workItem);
68+
}
3969
}

0 commit comments

Comments
 (0)