Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions docs/samples/components/AsyncComponentLoader.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
@if (isLoading)
{
<p>Loading...</p>
}
else
{
@foreach (var item in Items)
{
<ListItem Value="@item" />
}
}

@code {
[Parameter] public List<string> Items { get; set; } = new();

private bool isLoading = true;

protected override async Task OnInitializedAsync()
{
// Simulate async loading
await Task.Delay(100);
isLoading = false;
}
}
5 changes: 5 additions & 0 deletions docs/samples/components/ListItem.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<div class="list-item">@Value</div>

@code {
[Parameter] public string Value { get; set; } = string.Empty;
}
46 changes: 46 additions & 0 deletions docs/samples/tests/xunit/WaitForComponentTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using Bunit;
using Microsoft.Extensions.DependencyInjection;
using Xunit;

namespace Bunit.Docs.Samples;

public class WaitForComponentTest : BunitContext
{
[Fact]
public void WaitForComponent_WaitsForSingleComponent()
{
var cut = Render<AsyncComponentLoader>(parameters => parameters
.Add(p => p.Items, ["Item 1"]));

var listItem = cut.WaitForComponent<ListItem>();

Assert.Equal("Item 1", listItem.Find(".list-item").TextContent);
}

[Fact]
public void WaitForComponents_WaitsForMultipleComponents()
{
var items = new List<string> { "Item 1", "Item 2", "Item 3" };
var cut = Render<AsyncComponentLoader>(parameters => parameters
.Add(p => p.Items, items));

var listItems = cut.WaitForComponents<ListItem>();

Assert.Equal(3, listItems.Count);
Assert.Equal("Item 1", listItems.ElementAt(0).Find(".list-item").TextContent);
Assert.Equal("Item 2", listItems.ElementAt(1).Find(".list-item").TextContent);
Assert.Equal("Item 3", listItems.ElementAt(2).Find(".list-item").TextContent);
}

[Fact]
public void WaitForComponents_WaitsForSpecificCount()
{
var items = new List<string> { "Item 1", "Item 2", "Item 3", "Item 4", "Item 5" };
var cut = Render<AsyncComponentLoader>(parameters => parameters
.Add(p => p.Items, items));

var listItems = cut.WaitForComponents<ListItem>(5);

Assert.Equal(5, listItems.Count);
}
}
40 changes: 38 additions & 2 deletions docs/site/docs/interaction/awaiting-async-state.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ A test can fail if a component performs asynchronous renders. This may be due to

You need to handle this specifically in your tests because tests execute in the test framework's synchronization context and the test renderer executes renders in its own synchronization context. If you do not, you will likely experience tests that sometimes pass and sometimes fail.

bUnit comes with two methods that help to deal with this issue: the [`WaitForState()`](xref:Bunit.RenderedComponentWaitForHelperExtensions.WaitForState``1(Bunit.IRenderedComponent{``0},System.Func{System.Boolean},System.Nullable{System.TimeSpan})) method covered on this page, and the [`WaitForAssertion()`](xref:Bunit.RenderedComponentWaitForHelperExtensions.WaitForAssertion``1(Bunit.IRenderedComponent{``0},System.Action,System.Nullable{System.TimeSpan})) method covered on the <xref:async-assertion> page.
bUnit comes with several methods that help to deal with this issue: the [`WaitForState()`](xref:Bunit.RenderedComponentWaitForHelperExtensions.WaitForState``1(Bunit.IRenderedComponent{``0},System.Func{System.Boolean},System.Nullable{System.TimeSpan})) method, the [`WaitForAssertion()`](xref:Bunit.RenderedComponentWaitForHelperExtensions.WaitForAssertion``1(Bunit.IRenderedComponent{``0},System.Action,System.Nullable{System.TimeSpan})) method covered on the <xref:async-assertion> page, and the component-specific waiting methods [`WaitForComponent()`](xref:Bunit.RenderedComponentWaitForHelperExtensions.WaitForComponent``1(Bunit.IRenderedComponent{Microsoft.AspNetCore.Components.IComponent},System.Nullable{System.TimeSpan})) and [`WaitForComponents()`](xref:Bunit.RenderedComponentWaitForHelperExtensions.WaitForComponents``1(Bunit.IRenderedComponent{Microsoft.AspNetCore.Components.IComponent},System.Nullable{System.TimeSpan})) that are covered later on this page.

Let's start by taking a look at the `WaitForState` method in more detail.

Expand Down Expand Up @@ -48,6 +48,42 @@ If the timeout is reached, a <xref:Bunit.Extensions.WaitForHelpers.WaitForFailed

> The state predicate did not pass before the timeout period passed.

## Debugging code that uses `WaitForState`, `WaitForAssertion`, or `WaitForElement`
## Waiting for components using `WaitForComponent` and `WaitForComponents`

bUnit provides specialized methods for waiting for child components to appear in the render tree. These methods are useful when testing scenarios where components are rendered asynchronously based on some state change or data loading.

### Waiting for a single component

The [`WaitForComponent<TComponent>(TimeSpan?)`](xref:Bunit.RenderedComponentWaitForHelperExtensions.WaitForComponent``1(Bunit.IRenderedComponent{Microsoft.AspNetCore.Components.IComponent},System.Nullable{System.TimeSpan})) method waits until the specified component type is rendered in the DOM and returns the first instance found.

Consider the following `<AsyncComponentLoader>` component that loads child components asynchronously:

[!code-cshtml[AsyncComponentLoader.razor](../../../samples/components/AsyncComponentLoader.razor)]

To test that the `<ListItem>` components are rendered correctly, the `WaitForComponent<TComponent>()` method can be used:

[!code-csharp[WaitForComponentTest.cs](../../../samples/tests/xunit/WaitForComponentTest.cs?start=12&end=17)]

### Waiting for multiple components

The [`WaitForComponents<TComponent>(TimeSpan?)`](xref:Bunit.RenderedComponentWaitForHelperExtensions.WaitForComponents``1(Bunit.IRenderedComponent{Microsoft.AspNetCore.Components.IComponent},System.Nullable{System.TimeSpan})) method waits until at least one instance of the specified component type is rendered in the DOM and returns all instances found:

[!code-csharp[WaitForComponentTest.cs](../../../samples/tests/xunit/WaitForComponentTest.cs?start=23&end=31)]

### Waiting for a specific number of components

The [`WaitForComponents<TComponent>(int, TimeSpan?)`](xref:Bunit.RenderedComponentWaitForHelperExtensions.WaitForComponents``1(Bunit.IRenderedComponent{Microsoft.AspNetCore.Components.IComponent},System.Int32,System.Nullable{System.TimeSpan})) overload waits until at least the specified number of component instances are rendered:

[!code-csharp[WaitForComponentTest.cs](../../../samples/tests/xunit/WaitForComponentTest.cs?start=38&end=44)]

> [!NOTE]
> This method waits for **at least** the specified number of components, not exactly that number. If more components are rendered than requested, the method will still succeed and return all found instances.

> [!NOTE]
> These component wait methods use the same underlying mechanism as `WaitForState()` and will retry their checks every time the component under test renders.

If the timeout is reached, a <xref:Bunit.Extensions.WaitForHelpers.WaitForFailedException> exception is thrown with an appropriate error message indicating that the expected component(s) were not found within the timeout period.

## Debugging code that uses `WaitForState`, `WaitForAssertion`, `WaitForComponent`, `WaitForComponents`, or `WaitForElement`

When `bUnit` detects that a debugger is attached (`Debugger.IsAttached`), it will automatically disable the timeout functionality of the "wait for" methods.
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
namespace Bunit;

/// <summary>
/// Provides extension methods for waiting on components within a rendered Blazor component.
/// </summary>
public static partial class RenderedComponentWaitForHelperExtensions
{
/// <summary>
/// Waits until the specified component is rendered in the DOM.
/// </summary>
/// <param name="renderedComponent">The rendered component to find the component in.</param>
/// <param name="timeout">The maximum time to wait for the element to appear.</param>
/// <typeparam name="TComponent">The target component type to wait for.</typeparam>
/// <returns>See <see cref="IRenderedComponent{TComponent}"/>.</returns>
public static IRenderedComponent<TComponent> WaitForComponent<TComponent>(
this IRenderedComponent<IComponent> renderedComponent,
TimeSpan? timeout = null)
where TComponent : IComponent
{
renderedComponent.WaitForState(renderedComponent.HasComponent<TComponent>, timeout);
return renderedComponent.FindComponent<TComponent>();
}

/// <summary>
/// Waits until the specified number of components are rendered in the DOM and returns their instances.
/// </summary>
/// <param name="renderedComponent">The rendered component in which to search for instances of the specified component.</param>
/// <param name="matchComponentCount">The minimum amount component instances to wait for.</param>
/// <param name="timeout">The maximum time to wait for the components to appear. Defaults to no specific timeout if not provided.</param>
/// <typeparam name="TComponent">The target component type to wait for.</typeparam>
/// <returns>A read-only collection of <see cref="IRenderedComponent{TComponent}"/> instances representing the found components.</returns>
public static IReadOnlyCollection<IRenderedComponent<TComponent>> WaitForComponents<TComponent>(
this IRenderedComponent<IComponent> renderedComponent,
int matchComponentCount,
TimeSpan? timeout = null)
where TComponent : IComponent
{
renderedComponent.WaitForState(
() => renderedComponent.FindComponents<TComponent>().Count >= matchComponentCount,
timeout);
return renderedComponent.FindComponents<TComponent>();
}

/// <summary>
/// Waits until the specified component is rendered in the DOM and returns all instances of that component when the first instance is found.
/// </summary>
/// <param name="renderedComponent">The rendered component in which to search for instances of the specified component.</param>
/// <param name="timeout">The maximum time to wait for the components to appear. Defaults to no specific timeout if not provided.</param>
/// <typeparam name="TComponent">The target component type to wait for.</typeparam>
/// <returns>A read-only collection of <see cref="IRenderedComponent{TComponent}"/> instances representing the found components.</returns>
public static IReadOnlyCollection<IRenderedComponent<TComponent>> WaitForComponents<TComponent>(
this IRenderedComponent<IComponent> renderedComponent,
TimeSpan? timeout = null)
where TComponent : IComponent
=> renderedComponent.WaitForComponents<TComponent>(1, timeout);
}