Skip to content

Commit 3f730fa

Browse files
committed
Merge branch 'main' into dev
2 parents 7cc6e1f + 528573f commit 3f730fa

File tree

8 files changed

+320
-83
lines changed

8 files changed

+320
-83
lines changed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<p>@text</p>
2+
3+
@code
4+
{
5+
string text = string.Empty;
6+
[Parameter] public Task<string> TextService { get; set; }
7+
8+
protected override async Task OnInitializedAsync()
9+
{
10+
text = await TextService;
11+
}
12+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<output>@result</output>
2+
3+
@code
4+
{
5+
int result = 0;
6+
7+
public void Calculate(int x, int y)
8+
{
9+
result = x + y;
10+
StateHasChanged();
11+
}
12+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
using Microsoft.AspNetCore.Components.Web;
2+
using Xunit;
3+
using Bunit;
4+
using System;
5+
using System.Threading.Tasks;
6+
7+
namespace Bunit.Docs.Samples
8+
{
9+
public class AsyncDataTest
10+
{
11+
[Fact]
12+
public void LoadDataAsync()
13+
{
14+
// Arrange
15+
using var ctx = new TestContext();
16+
var textService = new TaskCompletionSource<string>();
17+
var cut = ctx.RenderComponent<AsyncData>(parameters => parameters
18+
.Add(p => p.TextService, textService.Task)
19+
);
20+
21+
// Act - set the awaited result from the text service
22+
textService.SetResult("Hello World");
23+
24+
// Wait for state before continuing test
25+
cut.WaitForState(() => cut.Find("p").TextContent == "Hello World");
26+
27+
// Assert - verify result has been set
28+
cut.MarkupMatches("<p>Hello World</p>");
29+
}
30+
31+
[Fact]
32+
public void LoadDataAsyncWithTimeout()
33+
{
34+
// Arrange
35+
using var ctx = new TestContext();
36+
var textService = new TaskCompletionSource<string>();
37+
var cut = ctx.RenderComponent<AsyncData>(parameters => parameters
38+
.Add(p => p.TextService, textService.Task)
39+
);
40+
41+
// Act - set the awaited result from the text service
42+
textService.SetResult("Long time");
43+
44+
// Wait for state before continuing test
45+
cut.WaitForState(() => cut.Find("p").TextContent == "Long time", TimeSpan.FromSeconds(2));
46+
47+
// Assert - verify result has been set
48+
cut.MarkupMatches("<p>Long time</p>");
49+
}
50+
51+
}
52+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
using Xunit;
2+
using Bunit;
3+
using System.Collections.Generic;
4+
using Microsoft.AspNetCore.Components;
5+
using Microsoft.AspNetCore.Components.Web;
6+
7+
using static Bunit.ComponentParameterFactory;
8+
9+
namespace Bunit.Docs.Samples
10+
{
11+
public class ReRenderTest
12+
{
13+
[Fact]
14+
public void RenderAgainUsingRender()
15+
{
16+
// Arrange - renders the Heading component
17+
using var ctx = new TestContext();
18+
var cut = ctx.RenderComponent<Heading>();
19+
Assert.Equal(1, cut.RenderCount);
20+
21+
// Re-render without new parameters
22+
cut.Render();
23+
24+
Assert.Equal(2, cut.RenderCount);
25+
}
26+
27+
[Fact]
28+
public void RenderAgainUsingSetParametersAndRender()
29+
{
30+
// Arrange - renders the Heading component
31+
using var ctx = new TestContext();
32+
var cut = ctx.RenderComponent<Item>(parameters => parameters
33+
.Add(p => p.Value, "Foo")
34+
);
35+
cut.MarkupMatches("<span>Foo</span>");
36+
37+
// Re-render with new parameters
38+
cut.SetParametersAndRender(parameters => parameters
39+
.Add(p => p.Value, "Bar")
40+
);
41+
42+
cut.MarkupMatches("<span>Bar</span>");
43+
}
44+
45+
[Fact]
46+
public void RendersViaInvokeAsync()
47+
{
48+
// Arrange - renders the Heading component
49+
using var ctx = new TestContext();
50+
var cut = ctx.RenderComponent<ImparativeCalc>();
51+
52+
// Indirectly re-renders through the call to StateHasChanged
53+
// in the Calculate(x, y) method.
54+
cut.InvokeAsync(() => cut.Instance.Calculate(1, 2));
55+
56+
cut.MarkupMatches("<output>3</output>");
57+
}
58+
}
59+
}

docs/site/docs/interaction/awaiting-async-state.md

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,42 @@ uid: awaiting-async-state
33
title: Awaiting an Asynchronous State Change in a Component Under Test
44
---
55

6-
# Awaiting an Asynchronous State Change
6+
# Awaiting an Asynchronous State Change
7+
8+
A test can fail if a component performs asynchronous renders, e.g. because it was awaiting an task to complete before continuing its render life-cycle. For example, if a component is waiting for a async web service to return data to it in the `OnInitializedAsync()` life-cycle method, before rendering it to the render tree.
9+
10+
This happens because tests execute in the test framework's synchronization context and the test renderer executes renders in its own synchronization context.
11+
12+
bUnit comes with two methods that helps deal with this issue, the [`WaitForState(Func<Boolean>, TimeSpan?)`](xref:Bunit.RenderedFragmentWaitForHelperExtensions.WaitForState(Bunit.IRenderedFragmentBase,System.Func{System.Boolean},System.Nullable{System.TimeSpan})) method covered on this page, and the [`WaitForAssertion(Action, TimeSpan?)`](xref:Bunit.RenderedFragmentWaitForHelperExtensions.WaitForAssertion(Bunit.IRenderedFragmentBase,System.Action,System.Nullable{System.TimeSpan})) method covered on the <xref:async-assertion> page.
13+
14+
## Waiting for state using `WaitForState`
15+
16+
The [`WaitForState(Func<Boolean>, TimeSpan?)`](xref:Bunit.RenderedFragmentWaitForHelperExtensions.WaitForState(Bunit.IRenderedFragmentBase,System.Func{System.Boolean},System.Nullable{System.TimeSpan})) method can be used to block and wait in a test method, until the provided predicate returns true, or the timeout is reached (the default timeout is one second).
17+
18+
Let us look at an example. Consider the following `<AsyncData>` component, who awaits an async `TextService` in its `OnInitializedAsync()` life-cycle method. When the service returns the data, the component will automatically re-render, to update its rendered markup.
19+
20+
[!code-html[AsyncData.razor](../../../samples/components/AsyncData.razor)]
21+
22+
To test the `<AsyncData>` component, do the following:
23+
24+
[!code-csharp[AsyncDataTest.cs](../../../samples/tests/xunit/AsyncDataTest.cs?start=15&end=28&highlight=2,8,11,14)]
25+
26+
This is what happens in the test:
27+
28+
1. The test uses a `TaskCompletionSource<string>` to simulate an async web service.
29+
2. In the second highlighted line, the result is provided to the component through the `textService`. This causes the component to re-render.
30+
3. In the third highlighted line, the `WaitForState()` method is used to block the test until the predicate provided to it returns true.
31+
4. Finally, the tests assertion step can execute, knowing that the desired state has been reached.
32+
33+
> [!NOTE]
34+
> The wait predicate and an assertion should not verify the same thing. Instead, use the [`WaitForAssertion(...)`](xref:Bunit.RenderedFragmentWaitForHelperExtensions.WaitForAssertion(Bunit.IRenderedFragmentBase,System.Action,System.Nullable{System.TimeSpan})) method covered on the <xref:async-assertion> page instead.
35+
36+
### Controlling wait timeout
37+
38+
The timeout, which defaults to one second, can be controlled by passing a `TimeSpan` as the second argument to the `WaitForState()` method, e.g.:
39+
40+
[!code-csharp[](../../../samples/tests/xunit/AsyncDataTest.cs?start=45&end=45)]
41+
42+
If the timeout is reached, a <xref:Bunit.Extensions.WaitForHelpers.WaitForFailedException> exception is thrown with the following error message:
43+
44+
> The state predicate did not pass before the timeout period passed.
Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,59 @@
11
---
22
uid: trigger-renders
3-
title: Triggering a Render Life-Cycle on a Component
3+
title: Triggering a Render Life Cycle on a Component
44
---
55

6-
# Triggering a Render Life-Cycle on a Component
6+
# Triggering a Render Life Cycle on a Component
77

8-
Describe how to trigger an explicit render life-cycle through the IRenderedComponent Render, SetParametersAndRender, and InvokeAsync methods.
8+
When a component under test is rendered, an instance of the <xref:Bunit.IRenderedComponent`1> type is returned. Through that, it is possible to cause the component under test to render again directly through the <xref:Bunit.IRenderedComponentBase`1.Render> method or one of the [`SetParametersAndRender(...)`](xref:Bunit.IRenderedComponentBase`1.SetParametersAndRender(Bunit.Rendering.ComponentParameter[])) methods or indirectly through the <xref:Bunit.IRenderedFragmentBase.InvokeAsync(System.Action)> method.
9+
10+
> [!WARNING]
11+
> The `Render()` and `SetParametersAndRender()` methods are not available in the <xref:Bunit.IRenderedFragment> type that is returned when calling the _non_-generic version of `GetComponentUnderTest()` in `<Fixture>`-based Razor tests. Call the generic version of `GetComponentUnderTest<TComponent>()` to get a <xref:Bunit.IRenderedComponent`1>.
12+
13+
> [!NOTE]
14+
> These methods are available and work the same in both C# and Razor-based tests. The examples below are from C# based tests only.
15+
16+
Let's look at how to use each of these methods to cause a re-render.
17+
18+
## Render
19+
20+
The <xref:Bunit.IRenderedComponentBase`1.Render> 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:
21+
22+
[!code-csharp[](../../../samples/tests/xunit/ReRenderTest.cs?start=17&end=24&highlight=6)]
23+
24+
The highlighted line shows the call to <xref:Bunit.IRenderedComponentBase`1.Render>.
25+
26+
> [!TIP]
27+
> The number of renders a component has been through can be inspected and verified using the <xref:Bunit.IRenderedFragmentBase.RenderCount> property.
28+
29+
## SetParametersAndRender
30+
31+
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:
32+
33+
[!code-csharp[](../../../samples/tests/xunit/ReRenderTest.cs?start=31&end=42&highlight=8-10)]
34+
35+
The highlighted line shows the call to <xref:Bunit.IRenderedComponentBase`1.SetParametersAndRender(System.Action{Bunit.ComponentParameterBuilder{`0}})>, which is also available as <xref:Bunit.IRenderedComponentBase`1.SetParametersAndRender(Bunit.Rendering.ComponentParameter[])> if you prefer that method of passing parameters.
36+
37+
> [!NOTE]
38+
> Passing parameters to components through the [`SetParametersAndRender(...)`](xref:Bunit.IRenderedComponentBase`1.SetParametersAndRender(Bunit.Rendering.ComponentParameter[])) methods is identical to doing it with the [`RenderComponent<TComponent>(...)`](xref:Bunit.IRenderedComponentBase`1.SetParametersAndRender(Bunit.Rendering.ComponentParameter[])) methods, described in detail on the <xref:passing-parameters-to-components> page.
39+
40+
## InvokeAsync
41+
42+
Invoking methods on a component under test, which causes a render, e.g. by calling `StateHasChanged`, can result in the following error:
43+
44+
> The current thread is not associated with the Dispatcher. Use InvokeAsync() to switch execution to the Dispatcher when triggering rendering or component state.
45+
46+
If you receive this error, you need to invoke your method inside an `Action` delegate passed to the <xref:Bunit.IRenderedFragmentBase.InvokeAsync(System.Action)> method.
47+
48+
Consider the `<ImparativeCalc>` component listed below:
49+
50+
[!code-html[ImparativeCalc.razor](../../../samples/components/ImparativeCalc.razor)]
51+
52+
To invoke the `Calculate()` method on the component instance, do the following:
53+
54+
[!code-csharp[](../../../samples/tests/xunit/ReRenderTest.cs?start=49&end=56&highlight=6)]
55+
56+
The highlighted line shows the call to <xref:Bunit.IRenderedFragmentBase.InvokeAsync(System.Action)>, which is passed an `Action` delegate, that calls the `Calculate` method.
57+
58+
> [!TIP]
59+
> The instance of a component under test is available through the <xref:Bunit.IRenderedComponentBase`1.Instance> property.

docs/site/docs/toc.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
# [Interaction](xref:interaction)
1414
## [Trigger Event Handlers](xref:trigger-event-handlers)
1515
## [Trigger Renders](xref:trigger-renders)
16-
## [Awaiting an Asynchronous State Change](xref:awaiting-async-state)
16+
## [Awaiting an Async State Change](xref:awaiting-async-state)
1717

1818
# [Verifying output](xref:verification)
1919
## [Verify Markup](xref:verify-markup)
@@ -22,7 +22,7 @@
2222
## [Asynchronous Assertion of Changes](xref:async-assertion)
2323

2424
# [Test Doubles](xref:test-doubles)
25-
## [Mocking Authorization](xref:mocking-auth)
25+
## [Faking Authorization](xref:faking-auth)
2626
## [Mocking HttpClient](xref:mocking-httpclient)
2727
## [Mocking IJsRuntime](xref:mocking-ijsruntime)
2828
## [Mocking Localization](xref:mocking-localizer)

0 commit comments

Comments
 (0)