diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml
index 7fc66628c..55c2b4309 100644
--- a/.github/workflows/prepare-release.yml
+++ b/.github/workflows/prepare-release.yml
@@ -5,15 +5,20 @@ on:
workflow_dispatch:
inputs:
versionIncrement:
- description: 'The version increment. Allowed values are "major" and "minor".'
+ description: 'The version increment. Allowed values are "major", "minor" and "build".'
+ type: choice
required: true
+ options:
+ - major
+ - minor
+ - build
default: 'minor'
jobs:
prepare-release:
name: ๐ Prepare new release
runs-on: ubuntu-latest
- if: github.ref == 'refs/heads/main' && contains(fromJson('["major","minor"]'), github.event.inputs.versionIncrement)
+ if: github.ref == 'refs/heads/main' && contains(fromJson('["major","minor","build"]'), github.event.inputs.versionIncrement)
steps:
- name: ๐ Checkout repository
uses: actions/checkout@v6
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 29d8a7e24..89d442c3b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,15 @@ All notable changes to **bUnit** will be documented in this file. The project ad
## [Unreleased]
+### Added
+
+- Added generic overloads `Find{TComponent, TElement}` and `FindAll{TComponent, TElement}` to query for specific element types (e.g., `IHtmlInputElement`). By [@linkdotnet](https://github.com/linkdotnet).
+- Added generic overloads `WaitForElement{TComponent, TElement}` and `WaitForElements{TComponent, TElement}` to wait for specific element types. By [@linkdotnet](https://github.com/linkdotnet).
+
+### Fixed
+
+- Adding convenient overloads for `InputAsync` and `ChangeAsync` to have feature parity with the sync version. Reported by [@ScarletKuro](https://github.com/ScarletKuro). Fixed by [@linkdotnet](https://github.com/linkdotnet).
+
## [2.2.2] - 2025-12-08
### Added
diff --git a/Directory.Packages.props b/Directory.Packages.props
index d99488398..bf8561613 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -29,29 +29,29 @@
-
+
-
-
+
+
-
-
-
+
+
+
-
-
-
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
diff --git a/MIGRATION.md b/MIGRATION.md
deleted file mode 100644
index 1c1c3a358..000000000
--- a/MIGRATION.md
+++ /dev/null
@@ -1,107 +0,0 @@
-# Migration Guide `v1` to `v2`
-This document describes the changes that need to be made to migrate from bUnit 1.x to 2.x.
-
-## Removal of `GetChangesSinceFirstRender` and `GetChangesSinceLastRender` methods
-The `GetChangesSinceFirstRender` and `GetChangesSinceLastRender` methods have been removed from `RenderedComponent`. There is no one-to-one replacement for these methods, but the general idea is to select the HTML in question via `Find` and assert against that.
-
-Alternatively, the `RenderedFragment` still offers the `OnMarkupUpdated` event, which can be used to assert against the markup after a render.
-
-## Removal of `IsNullOrEmpty` extension method on `IEnumerable` and `CreateLogger` on `IServiceProvider`
-The `IsNullOrEmpty` extension method on `IEnumerable` has been removed, as well as the `CreateLogger` extension method on `IServiceProvider`. These extension methods are pretty common and conflict with other libraries. These methods can be recreated like this:
-
-```csharp
-public static class Extensions
-{
- public static bool IsNullOrEmpty(this IEnumerable enumerable)
- => enumerable == null || !enumerable.Any();
-
- public static ILogger CreateLogger(this IServiceProvider serviceProvider)
- {
- var loggerFactory = serviceProvider.GetRequiredService() ?? NullLoggerFactory.Instance;
- return loggerFactory.CreateLogger();
- }
-}
-```
-
-## Merge of `bunit.core` and `bunit.web`
-The `bunit.core` and `bunit.web` packages have been merged into a single `bunit` package. If you used either of these packages, you should remove them and install the `bunit` package instead.
-
-## Removal of unneeded abstraction
-
-### `IRenderedComponentBase` and `RenderedFragmentBase`
-`IRenderedComponentBase`, `IRenderedComponent`, `RenderedFragmentBase`, `RenderedFragment` and `RenderedFragmentBase` have been removed.
-If you used either of these types, you should replace them with `RenderedComponent` or `RenderedFragment` respectively.
-
-### `WebTestRender` merged into `BunitTestRender`
-The `WebTestRender` class has been merged into the `TestRender` class. If you used `WebTestRender`, you should replace it with `BunitTestRender`.
-
-## Renamed `Fake` to `Bunit` in many test doubles
-The `Fake` prefix has been replaced with `Bunit` in many test doubles. For example, `FakeNavigationManager` is now `BunitNavigationManager`. If you reference any of these types explicitly, you need to update your code.
-
-### Renamed `AddTestAuthorization` to `AddAuthorization`
-The `AddTestAuthorization` method on `BunitContext` has been renamed to `AddAuthorization`. If you used `AddTestAuthorization`, you should replace it with `AddAuthorization`.
-
-## Merged `BunitContext` and `BunitContextBase`
-The `BunitContext` and `BunitContextBase` classes have been merged into a single `BunitContext` class. All references to `BunitContextBase` should replace them with `BunitContext` to migrate.
-
-## Renamed all `RenderComponent` and `SetParametersAndRender` to `Render`
-To make the API more consistent, `RenderComponent` and `SetParametersAndRender` methods have been renamed to `Render`.
-
-## Removal of `ComponentParameter` and method using them
-Using `ComponentParameter` and factory methods to create them is not recommend in V1 and have now been removed in V2. Instead, use the strongly typed builder pattern that enables you to pass parameters to components you render.
-
-## `BunitContext` implements `IDisposable` and `IAsyncDisposable`
-The `BunitContext` now implements `IDisposable` and `IAsyncDisposable`. In version 1.x, `BunitContext` only implemented `IDisposable` and cleaned up asynchronous objects in the synchronous `Dispose` method. This is no longer the case, and asynchronous objects are now cleaned up in the `DisposeAsync` method.
-If you register services into the container that implement `IAsyncDisposable` make sure that the test framework calls the right method.
-
-## `TestContext` was renamed to `BunitContext`
-The `TestContext` class has been renamed to `BunitContext`. If you used `TestContext`, you should replace it with `BunitContext`.
-
-## `TestContextWrapper` was removed
-The `TestContextWrapper` class has been removed. Either use lifecycle events of the testing framework (like `LifeCycle.InstancePerTestCase` in NUnit).
-```csharp
-[FixtureLifeCycle(LifeCycle.InstancePerTestCase)]
-public class HelloWorldInstancePerTestCase : Bunit.TestContext
-{
- [Test]
- public void HelloWorldComponentRendersCorrectly()
- {
- // Act
- var cut = RenderComponent();
-
- // Assert
- cut.MarkupMatches("
Hello world from Blazor
");
- }
-}
-```
-
-Or use the `BunitContext` directly and manage the lifecycle yourself.
-
-## `TestServiceProvider` renamed to `BunitTestServiceProvider`
-The `TestServiceProvider` class has been renamed to `BunitTestServiceProvider`. If you used `TestServiceProvider`, you should replace it with `BunitTestServiceProvider`.
-
-## `DisposeComponents` is now asynchronous and called `DisposeComponentsAsync`
-`DisposeComponentsAsync` allows to await `DisposeAsync` of components under test. If you used `DisposeComponents`, you should replace it with `DisposeComponentsAsync`.
-
-## `IRefreshableElementCollection` was removed
-
-The `IRefreshableElementCollection` interface has been removed. With this the `FindAll` method does not accept a `bool enableRefresh` parameter anymore. Code like this:
-
-```csharp
-var items = cut.FindAll("li", enableRefresh: true);
-
-cut.Find("button").Click(); // Some action that causes items to change
-
-Assert.Equal(3, items.Count);
-```
-
-Should be changed to this:
-
-```csharp
-var items = cut.FindAll("li");
-
-cut.Find("button").Click(); // Some action that causes items to change
-
-items = cut.FindAll("li"); // Re-query the items
-Assert.Equal(3, items.Count);
-```
\ No newline at end of file
diff --git a/bunit.slnx b/bunit.slnx
index 7294d3ede..8c453b444 100644
--- a/bunit.slnx
+++ b/bunit.slnx
@@ -11,7 +11,6 @@
-
diff --git a/docs/site/docs/migrations/1to2.md b/docs/site/docs/migrations/1to2.md
index 16bb88e9d..0555f58a2 100644
--- a/docs/site/docs/migrations/1to2.md
+++ b/docs/site/docs/migrations/1to2.md
@@ -78,4 +78,38 @@ cut.Find("button").Click(detail: 2, ctrlKey: true);
The last one was a method with all parameters of `MouseEventArgs` as optional parameters. This method has been removed in favor of using the `MouseEventArgs` directly.
-Also `ClickAsync` - to align with its synchronous counterpart - doesn't take `MouseEventArgs` as mandatory parameter anymore. If not set, a default instance will be created.
\ No newline at end of file
+Also `ClickAsync` - to align with its synchronous counterpart - doesn't take `MouseEventArgs` as mandatory parameter anymore. If not set, a default instance will be created.
+
+## `DisposeComponents` is now async and called `DisposeComponentsAsync`
+
+The `DisposeComponents` method has been renamed to `DisposeComponentsAsync` and is now asynchronous. To migrate, simply rename the method and add `await`:
+
+```diff
+- DisposeComponents();
++ await DisposeComponentsAsync();
+```
+
+##ย The `ComponentParameterFactory` and `ComponentParameter` has been removed
+The `ComponentParameterFactory` class has been removed (and therefore the usage of `ComponentParameter`).
+Instead, use the `Render` method (and its overloads) to pass parameters to components.
+
+## `IRefreshableElementCollection` was removed
+The `IRefreshableElementCollection` interface has been removed. With that, the overload in `FindAll` doesn't accept a `bool refresh` parameter anymore. Instead, simply call `FindAll` again to get a refreshed collection.
+
+```csharp
+var items = cut.FindAll("li", refresh: true);
+items.Count.ShouldBe(3);
+cut.Find("button").Click(); // This changes the list items
+
+items.Count.ShouldBe(4);
+```
+
+Should be changed to:
+
+```csharp
+var items = cut.FindAll("li");
+items.Count.ShouldBe(3);
+cut.Find("button").Click(); // This changes the list items
+items = cut.FindAll("li"); // Call FindAll again to refresh
+items.Count.ShouldBe(4);
+```
\ No newline at end of file
diff --git a/src/Directory.Build.props b/src/Directory.Build.props
index 89a13faaa..ed6fe5bd1 100644
--- a/src/Directory.Build.props
+++ b/src/Directory.Build.props
@@ -26,7 +26,7 @@
truetrue
- 1.25.3
+ 2.0.66
diff --git a/src/bunit/EventDispatchExtensions/InputEventDispatchExtensions.cs b/src/bunit/EventDispatchExtensions/InputEventDispatchExtensions.cs
index bae8697fc..bfa7d9f70 100644
--- a/src/bunit/EventDispatchExtensions/InputEventDispatchExtensions.cs
+++ b/src/bunit/EventDispatchExtensions/InputEventDispatchExtensions.cs
@@ -9,7 +9,7 @@ namespace Bunit;
public static partial class EventHandlerDispatchExtensions
{
///
- /// Raises the @onchange event on , passing the provided
+ /// Raises the @onchange event on , passing the provided
/// properties to the event handler via a object.
///
/// The element to raise the event on.
@@ -18,7 +18,16 @@ public static void Change(this IElement element, T value)
=> _ = ChangeAsync(element, CreateFrom(value));
///
- /// Raises the @oninput event on , passing the provided
+ /// Raises the @onchange event on , passing the provided
+ /// properties to the event handler via a object.
+ ///
+ /// The element to raise the event on.
+ /// The new value.
+ public static void ChangeAsync(this IElement element, T value)
+ => _ = ChangeAsync(element, CreateFrom(value));
+
+ ///
+ /// Raises the @oninput event on , passing the provided
/// properties to the event handler via a object.
///
/// The element to raise the event on.
@@ -26,6 +35,15 @@ public static void Change(this IElement element, T value)
public static void Input(this IElement element, T value)
=> _ = InputAsync(element, CreateFrom(value));
+ ///
+ /// Raises the @oninput event on , passing the provided
+ /// properties to the event handler via a object.
+ ///
+ /// The element to raise the event on.
+ /// The new value.
+ public static void InputAsync(this IElement element, T value)
+ => _ = InputAsync(element, CreateFrom(value));
+
private static ChangeEventArgs CreateFrom(T value) => new() { Value = FormatValue(value) };
private static object? FormatValue(T value)
diff --git a/src/bunit/Extensions/RenderedComponentExtensions.cs b/src/bunit/Extensions/RenderedComponentExtensions.cs
index 199ae3316..55a85d27b 100644
--- a/src/bunit/Extensions/RenderedComponentExtensions.cs
+++ b/src/bunit/Extensions/RenderedComponentExtensions.cs
@@ -18,6 +18,21 @@ public static class RenderedComponentExtensions
/// The group of selectors to use.
public static IElement Find(this IRenderedComponent renderedComponent, string cssSelector)
where TComponent : IComponent
+ => Find(renderedComponent, cssSelector);
+
+ ///
+ /// Returns the first element of type from the rendered fragment or component under test,
+ /// using the provided , in a depth-first pre-order traversal
+ /// of the rendered nodes.
+ ///
+ /// The type of the component under test.
+ /// The type of element to find (e.g., IHtmlInputElement).
+ /// The rendered fragment to search.
+ /// The group of selectors to use.
+ /// Thrown if no element matches the .
+ public static TElement Find(this IRenderedComponent renderedComponent, string cssSelector)
+ where TComponent : IComponent
+ where TElement : class, IElement
{
ArgumentNullException.ThrowIfNull(renderedComponent);
@@ -26,7 +41,11 @@ public static IElement Find(this IRenderedComponent rend
if (result is null)
throw new ElementNotFoundException(cssSelector);
- return result.WrapUsing(new CssSelectorElementFactory((IRenderedComponent)renderedComponent, cssSelector));
+ if (result is not TElement)
+ throw new ElementNotFoundException(
+ $"The element matching '{cssSelector}' is of type '{result.GetType().Name}', not '{typeof(TElement).Name}'.");
+
+ return (TElement)result.WrapUsing(new CssSelectorElementFactory((IRenderedComponent)renderedComponent, cssSelector));
}
///
@@ -39,10 +58,25 @@ public static IElement Find(this IRenderedComponent rend
/// An , that can be refreshed to execute the search again.
public static IReadOnlyList FindAll(this IRenderedComponent renderedComponent, string cssSelector)
where TComponent : IComponent
+ => FindAll(renderedComponent, cssSelector);
+
+ ///
+ /// Returns a collection of elements of type from the rendered fragment or component under test,
+ /// using the provided , in a depth-first pre-order traversal
+ /// of the rendered nodes. Only elements matching the type are returned.
+ ///
+ /// The type of the component under test.
+ /// The type of elements to find (e.g., IHtmlInputElement).
+ /// The rendered fragment to search.
+ /// The group of selectors to use.
+ /// An containing only elements matching the specified type.
+ public static IReadOnlyList FindAll(this IRenderedComponent renderedComponent, string cssSelector)
+ where TComponent : IComponent
+ where TElement : class, IElement
{
ArgumentNullException.ThrowIfNull(renderedComponent);
- return renderedComponent.Nodes.QuerySelectorAll(cssSelector).ToArray();
+ return renderedComponent.Nodes.QuerySelectorAll(cssSelector).OfType().ToArray();
}
///
diff --git a/src/bunit/Extensions/WaitForHelpers/RenderedComponentWaitForHelperExtensions.WaitForElement.cs b/src/bunit/Extensions/WaitForHelpers/RenderedComponentWaitForHelperExtensions.WaitForElement.cs
index 9e1e56a14..454778f74 100644
--- a/src/bunit/Extensions/WaitForHelpers/RenderedComponentWaitForHelperExtensions.WaitForElement.cs
+++ b/src/bunit/Extensions/WaitForHelpers/RenderedComponentWaitForHelperExtensions.WaitForElement.cs
@@ -33,6 +33,37 @@ public static IElement WaitForElement(this IRenderedComponent WaitForElementCore(renderedComponent, cssSelector, timeout: timeout);
+ ///
+ /// Wait until an element of type matching the exists in the ,
+ /// or the timeout is reached (default is one second).
+ ///
+ /// The type of the component under test.
+ /// The type of element to wait for (e.g., IHtmlInputElement).
+ /// The render fragment or component find the matching element in.
+ /// The CSS selector to use to search for the element.
+ /// Thrown if no elements is found matching the within the default timeout. See the inner exception for details.
+ /// The .
+ public static TElement WaitForElement(this IRenderedComponent renderedComponent, string cssSelector)
+ where TComponent : IComponent
+ where TElement : class, IElement
+ => WaitForElementCore(renderedComponent, cssSelector, timeout: null);
+
+ ///
+ /// Wait until an element of type matching the exists in the ,
+ /// or the is reached.
+ ///
+ /// The type of the component under test.
+ /// The type of element to wait for (e.g., IHtmlInputElement).
+ /// The render fragment or component find the matching element in.
+ /// The CSS selector to use to search for the element.
+ /// The maximum time to wait for the element to appear.
+ /// Thrown if no elements is found matching the within the default timeout. See the inner exception for details.
+ /// The .
+ public static TElement WaitForElement(this IRenderedComponent renderedComponent, string cssSelector, TimeSpan timeout)
+ where TComponent : IComponent
+ where TElement : class, IElement
+ => WaitForElementCore(renderedComponent, cssSelector, timeout: timeout);
+
///
/// Wait until at least one element matching the exists in the ,
/// or the timeout is reached (default is one second).
@@ -83,6 +114,70 @@ public static IReadOnlyList WaitForElements(this IRendered
where TComponent : IComponent
=> WaitForElementsCore(renderedComponent, cssSelector, matchElementCount: matchElementCount, timeout: timeout);
+ ///
+ /// Wait until at least one element of type matching the exists in the ,
+ /// or the timeout is reached (default is one second).
+ ///
+ /// The type of the component under test.
+ /// The type of elements to wait for (e.g., IHtmlInputElement).
+ /// The render fragment or component find the matching element in.
+ /// The CSS selector to use to search for elements.
+ /// Thrown if no elements is found matching the within the default timeout.
+ /// The .
+ public static IReadOnlyList WaitForElements(this IRenderedComponent renderedComponent, string cssSelector)
+ where TComponent : IComponent
+ where TElement : class, IElement
+ => WaitForElementsCore(renderedComponent, cssSelector, matchElementCount: null, timeout: null);
+
+ ///
+ /// Wait until exactly element(s) of type matching the exists in the ,
+ /// or the timeout is reached (default is one second).
+ ///
+ /// The type of the component under test.
+ /// The type of elements to wait for (e.g., IHtmlInputElement).
+ /// The render fragment or component find the matching element in.
+ /// The CSS selector to use to search for elements.
+ /// The exact number of elements to that the should match.
+ /// Thrown if no elements is found matching the within the default timeout.
+ /// The .
+ public static IReadOnlyList WaitForElements(this IRenderedComponent renderedComponent, string cssSelector, int matchElementCount)
+ where TComponent : IComponent
+ where TElement : class, IElement
+ => WaitForElementsCore(renderedComponent, cssSelector, matchElementCount: matchElementCount, timeout: null);
+
+ ///
+ /// Wait until at least one element of type matching the exists in the ,
+ /// or the is reached.
+ ///
+ /// The type of the component under test.
+ /// The type of elements to wait for (e.g., IHtmlInputElement).
+ /// The render fragment or component find the matching element in.
+ /// The CSS selector to use to search for elements.
+ /// The maximum time to wait for elements to appear.
+ /// Thrown if no elements is found matching the within the default timeout.
+ /// The .
+ public static IReadOnlyList WaitForElements(this IRenderedComponent renderedComponent, string cssSelector, TimeSpan timeout)
+ where TComponent : IComponent
+ where TElement : class, IElement
+ => WaitForElementsCore(renderedComponent, cssSelector, matchElementCount: null, timeout: timeout);
+
+ ///
+ /// Wait until exactly element(s) of type matching the exists in the ,
+ /// or the is reached.
+ ///
+ /// The type of the component under test.
+ /// The type of elements to wait for (e.g., IHtmlInputElement).
+ /// The render fragment or component find the matching element in.
+ /// The CSS selector to use to search for elements.
+ /// The exact number of elements to that the should match.
+ /// The maximum time to wait for elements to appear.
+ /// Thrown if no elements is found matching the within the default timeout.
+ /// The .
+ public static IReadOnlyList WaitForElements(this IRenderedComponent renderedComponent, string cssSelector, int matchElementCount, TimeSpan timeout)
+ where TComponent : IComponent
+ where TElement : class, IElement
+ => WaitForElementsCore(renderedComponent, cssSelector, matchElementCount: matchElementCount, timeout: timeout);
+
///
/// Wait until an element matching the exists in the ,
/// or the timeout is reached (default is one second).
@@ -107,6 +202,37 @@ public static Task WaitForElementAsync(this IRenderedCompo
where TComponent : IComponent
=> WaitForElementCoreAsync(renderedComponent, cssSelector, timeout: timeout);
+ ///
+ /// Wait until an element of type matching the exists in the ,
+ /// or the timeout is reached (default is one second).
+ ///
+ /// The type of the component under test.
+ /// The type of element to wait for (e.g., IHtmlInputElement).
+ /// The render fragment or component find the matching element in.
+ /// The CSS selector to use to search for the element.
+ /// Thrown if no elements is found matching the within the default timeout. See the inner exception for details.
+ /// The .
+ public static Task WaitForElementAsync(this IRenderedComponent renderedComponent, string cssSelector)
+ where TComponent : IComponent
+ where TElement : class, IElement
+ => WaitForElementCoreAsync(renderedComponent, cssSelector, timeout: null);
+
+ ///
+ /// Wait until an element of type matching the exists in the ,
+ /// or the is reached.
+ ///
+ /// The type of the component under test.
+ /// The type of element to wait for (e.g., IHtmlInputElement).
+ /// The render fragment or component find the matching element in.
+ /// The CSS selector to use to search for the element.
+ /// The maximum time to wait for the element to appear.
+ /// Thrown if no elements is found matching the within the default timeout. See the inner exception for details.
+ /// The .
+ public static Task WaitForElementAsync(this IRenderedComponent renderedComponent, string cssSelector, TimeSpan timeout)
+ where TComponent : IComponent
+ where TElement : class, IElement
+ => WaitForElementCoreAsync(renderedComponent, cssSelector, timeout: timeout);
+
///
/// Wait until exactly element(s) matching the exists in the ,
/// or the timeout is reached (default is one second).
@@ -157,12 +283,80 @@ public static Task> WaitForElementsAsync(thi
/// The .
public static Task> WaitForElementsAsync(this IRenderedComponent renderedComponent, string cssSelector)
where TComponent : IComponent
- => WaitForElementsCoreAsync(renderedComponent, cssSelector, matchElementCount: null, timeout: null);
+ => WaitForElementsCoreAsync(renderedComponent, cssSelector, matchElementCount: null, timeout: null);
+ ///
+ /// Wait until at least one element of type matching the exists in the ,
+ /// or the timeout is reached (default is one second).
+ ///
+ /// The type of the component under test.
+ /// The type of elements to wait for (e.g., IHtmlInputElement).
+ /// The render fragment or component find the matching element in.
+ /// The CSS selector to use to search for elements.
+ /// Thrown if no elements is found matching the within the default timeout.
+ /// The .
+ public static Task> WaitForElementsAsync(this IRenderedComponent renderedComponent, string cssSelector)
+ where TComponent : IComponent
+ where TElement : class, IElement
+ => WaitForElementsCoreAsync(renderedComponent, cssSelector, matchElementCount: null, timeout: null);
+
+ ///
+ /// Wait until exactly element(s) of type matching the exists in the ,
+ /// or the timeout is reached (default is one second).
+ ///
+ /// The type of the component under test.
+ /// The type of elements to wait for (e.g., IHtmlInputElement).
+ /// The render fragment or component find the matching element in.
+ /// The CSS selector to use to search for elements.
+ /// The exact number of elements to that the should match.
+ /// Thrown if no elements is found matching the within the default timeout.
+ /// The .
+ public static Task> WaitForElementsAsync(this IRenderedComponent renderedComponent, string cssSelector, int matchElementCount)
+ where TComponent : IComponent
+ where TElement : class, IElement
+ => WaitForElementsCoreAsync(renderedComponent, cssSelector, matchElementCount: matchElementCount, timeout: null);
+
+ ///
+ /// Wait until at least one element of type matching the exists in the ,
+ /// or the is reached.
+ ///
+ /// The type of the component under test.
+ /// The type of elements to wait for (e.g., IHtmlInputElement).
+ /// The render fragment or component find the matching element in.
+ /// The CSS selector to use to search for elements.
+ /// The maximum time to wait for elements to appear.
+ /// Thrown if no elements is found matching the within the default timeout.
+ /// The .
+ public static Task> WaitForElementsAsync(this IRenderedComponent renderedComponent, string cssSelector, TimeSpan timeout)
+ where TComponent : IComponent
+ where TElement : class, IElement
+ => WaitForElementsCoreAsync(renderedComponent, cssSelector, matchElementCount: null, timeout: timeout);
+
+ ///
+ /// Wait until exactly element(s) of type matching the exists in the ,
+ /// or the is reached.
+ ///
+ /// The type of the component under test.
+ /// The type of elements to wait for (e.g., IHtmlInputElement).
+ /// The render fragment or component find the matching element in.
+ /// The CSS selector to use to search for elements.
+ /// The exact number of elements to that the should match.
+ /// The maximum time to wait for elements to appear.
+ /// Thrown if no elements is found matching the within the default timeout.
+ /// The .
+ public static Task> WaitForElementsAsync(this IRenderedComponent renderedComponent, string cssSelector, int matchElementCount, TimeSpan timeout)
+ where TComponent : IComponent
+ where TElement : class, IElement
+ => WaitForElementsCoreAsync(renderedComponent, cssSelector, matchElementCount: matchElementCount, timeout: timeout);
private static IElement WaitForElementCore(this IRenderedComponent renderedComponent, string cssSelector, TimeSpan? timeout)
where TComponent : IComponent
+ => WaitForElementCore(renderedComponent, cssSelector, timeout);
+
+ private static TElement WaitForElementCore(this IRenderedComponent renderedComponent, string cssSelector, TimeSpan? timeout)
+ where TComponent : IComponent
+ where TElement : class, IElement
{
- using var waiter = new WaitForElementHelper(renderedComponent, cssSelector, timeout);
+ using var waiter = new WaitForElementHelper(renderedComponent, cssSelector, timeout);
try
{
@@ -177,10 +371,15 @@ private static IElement WaitForElementCore(this IRenderedComponent WaitForElementCoreAsync(this IRenderedComponent renderedComponent, string cssSelector, TimeSpan? timeout)
+ private static Task WaitForElementCoreAsync(this IRenderedComponent renderedComponent, string cssSelector, TimeSpan? timeout)
+ where TComponent : IComponent
+ => WaitForElementCoreAsync(renderedComponent, cssSelector, timeout);
+
+ private static async Task WaitForElementCoreAsync(this IRenderedComponent renderedComponent, string cssSelector, TimeSpan? timeout)
where TComponent : IComponent
+ where TElement : class, IElement
{
- using var waiter = new WaitForElementHelper(renderedComponent, cssSelector, timeout);
+ using var waiter = new WaitForElementHelper(renderedComponent, cssSelector, timeout);
return await waiter.WaitTask;
}
@@ -191,8 +390,17 @@ private static IReadOnlyList WaitForElementsCore(
int? matchElementCount,
TimeSpan? timeout)
where TComponent : IComponent
+ => WaitForElementsCore(renderedComponent, cssSelector, matchElementCount, timeout);
+
+ private static IReadOnlyList WaitForElementsCore(
+ this IRenderedComponent renderedComponent,
+ string cssSelector,
+ int? matchElementCount,
+ TimeSpan? timeout)
+ where TComponent : IComponent
+ where TElement : class, IElement
{
- using var waiter = new WaitForElementsHelper(renderedComponent, cssSelector, matchElementCount, timeout);
+ using var waiter = new WaitForElementsHelper(renderedComponent, cssSelector, matchElementCount, timeout);
try
{
@@ -207,14 +415,23 @@ private static IReadOnlyList WaitForElementsCore(
}
}
- private static async Task> WaitForElementsCoreAsync(
+ private static Task> WaitForElementsCoreAsync(
+ this IRenderedComponent renderedComponent,
+ string cssSelector,
+ int? matchElementCount,
+ TimeSpan? timeout)
+ where TComponent : IComponent
+ => WaitForElementsCoreAsync(renderedComponent, cssSelector, matchElementCount, timeout);
+
+ private static async Task> WaitForElementsCoreAsync(
this IRenderedComponent renderedComponent,
string cssSelector,
int? matchElementCount,
TimeSpan? timeout)
where TComponent : IComponent
+ where TElement : class, IElement
{
- using var waiter = new WaitForElementsHelper(renderedComponent, cssSelector, matchElementCount, timeout);
+ using var waiter = new WaitForElementsHelper(renderedComponent, cssSelector, matchElementCount, timeout);
return await waiter.WaitTask;
}
diff --git a/src/bunit/Extensions/WaitForHelpers/WaitForElementHelper.cs b/src/bunit/Extensions/WaitForHelpers/WaitForElementHelper.cs
index 316ead9d1..70fd9cf35 100644
--- a/src/bunit/Extensions/WaitForHelpers/WaitForElementHelper.cs
+++ b/src/bunit/Extensions/WaitForHelpers/WaitForElementHelper.cs
@@ -5,8 +5,21 @@ namespace Bunit.Extensions.WaitForHelpers;
///
/// Represents an async wait helper, that will wait for a specified time for an element to become available in the DOM.
///
-internal class WaitForElementHelper : WaitForHelper
+internal class WaitForElementHelper : WaitForElementHelper
where TComponent : IComponent
+{
+ public WaitForElementHelper(IRenderedComponent renderedComponent, string cssSelector, TimeSpan? timeout = null)
+ : base(renderedComponent, cssSelector, timeout)
+ {
+ }
+}
+
+///
+/// Represents an async wait helper, that will wait for a specified time for an element of type to become available in the DOM.
+///
+internal class WaitForElementHelper : WaitForHelper
+ where TComponent : IComponent
+ where TElement : class, IElement
{
internal const string TimeoutBeforeFoundMessage = "The CSS selector and/or predicate did not result in a matching element before the timeout period passed.";
@@ -19,7 +32,7 @@ internal class WaitForElementHelper : WaitForHelper renderedComponent, string cssSelector, TimeSpan? timeout = null)
: base(renderedComponent, () =>
{
- var element = renderedComponent.Find(cssSelector);
+ var element = renderedComponent.Find(cssSelector);
return (true, element);
}, timeout)
{
diff --git a/src/bunit/Extensions/WaitForHelpers/WaitForElementsHelper.cs b/src/bunit/Extensions/WaitForHelpers/WaitForElementsHelper.cs
index e53ab8dfb..ec50c9fd4 100644
--- a/src/bunit/Extensions/WaitForHelpers/WaitForElementsHelper.cs
+++ b/src/bunit/Extensions/WaitForHelpers/WaitForElementsHelper.cs
@@ -7,8 +7,21 @@ namespace Bunit.Extensions.WaitForHelpers;
///
/// Represents an async wait helper, that will wait for a specified time for element(s) to become available in the DOM.
///
-internal class WaitForElementsHelper : WaitForHelper, TComponent>
+internal class WaitForElementsHelper : WaitForElementsHelper
where TComponent : IComponent
+{
+ public WaitForElementsHelper(IRenderedComponent renderedComponent, string cssSelector, int? matchElementCount, TimeSpan? timeout = null)
+ : base(renderedComponent, cssSelector, matchElementCount, timeout)
+ {
+ }
+}
+
+///
+/// Represents an async wait helper, that will wait for a specified time for element(s) of type to become available in the DOM.
+///
+internal class WaitForElementsHelper : WaitForHelper, TComponent>
+ where TComponent : IComponent
+ where TElement : class, IElement
{
internal const string TimeoutBeforeFoundMessage = "The CSS selector did not result in any matching element(s) before the timeout period passed.";
internal static readonly CompositeFormat TimeoutBeforeFoundWithCountMessage = CompositeFormat.Parse("The CSS selector did not result in exactly {0} matching element(s) before the timeout period passed.");
@@ -25,7 +38,7 @@ internal class WaitForElementsHelper : WaitForHelper renderedComponent, string cssSelector, int? matchElementCount, TimeSpan? timeout = null)
: base(renderedComponent, () =>
{
- var elements = renderedComponent.FindAll(cssSelector);
+ var elements = renderedComponent.FindAll(cssSelector);
var checkPassed = matchElementCount is null
? elements.Count > 0
diff --git a/tests/bunit.tests/Extensions/WaitForHelpers/RenderedComponentWaitForElementsHelperExtensionsTest.cs b/tests/bunit.tests/Extensions/WaitForHelpers/RenderedComponentWaitForElementsHelperExtensionsTest.cs
index e7f4b844b..295fdf844 100644
--- a/tests/bunit.tests/Extensions/WaitForHelpers/RenderedComponentWaitForElementsHelperExtensionsTest.cs
+++ b/tests/bunit.tests/Extensions/WaitForHelpers/RenderedComponentWaitForElementsHelperExtensionsTest.cs
@@ -1,4 +1,5 @@
using System.Globalization;
+using AngleSharp.Html.Dom;
namespace Bunit.Extensions.WaitForHelpers;
@@ -97,4 +98,55 @@ public void Test025()
elms.ShouldBeEmpty();
}
+
+ [Fact(DisplayName = "WaitForElement waits until element of specified type matching cssSelector appears")]
+ [Trait("Category", "sync")]
+ public void Test026()
+ {
+ var expectedMarkup = "";
+ var cut = Render(ps => ps.AddChildContent(expectedMarkup));
+
+ var elm = cut.WaitForElement("#myInput");
+
+ elm.ShouldNotBeNull();
+ elm.Type.ShouldBe("text");
+ }
+
+ [Fact(DisplayName = "WaitForElement throws exception when element type does not match")]
+ [Trait("Category", "sync")]
+ public void Test027()
+ {
+ var cut = Render(ps => ps.AddChildContent(""));
+
+ var expected = Should.Throw(() =>
+ cut.WaitForElement("#myDiv", WaitForTestTimeout));
+
+ expected.InnerException.ShouldBeOfType();
+ }
+
+ [Fact(DisplayName = "WaitForElements waits until elements of specified type matching cssSelector appear")]
+ [Trait("Category", "sync")]
+ public void Test028()
+ {
+ var expectedMarkup = "";
+ var cut = Render(ps => ps.AddChildContent(expectedMarkup));
+
+ var elms = cut.WaitForElements("main input");
+
+ elms.Count.ShouldBe(2);
+ elms[0].ShouldBeAssignableTo();
+ elms[1].ShouldBeAssignableTo();
+ }
+
+ [Fact(DisplayName = "WaitForElements with count waits until exactly N elements of specified type appear")]
+ [Trait("Category", "sync")]
+ public void Test029()
+ {
+ var expectedMarkup = "";
+ var cut = Render(ps => ps.AddChildContent(expectedMarkup));
+
+ var elms = cut.WaitForElements("main input", matchElementCount: 3);
+
+ elms.Count.ShouldBe(3);
+ }
}
diff --git a/tests/bunit.tests/Rendering/RenderedComponentTest.cs b/tests/bunit.tests/Rendering/RenderedComponentTest.cs
index 2c1866495..bdf04d193 100644
--- a/tests/bunit.tests/Rendering/RenderedComponentTest.cs
+++ b/tests/bunit.tests/Rendering/RenderedComponentTest.cs
@@ -1,4 +1,5 @@
using AngleSharp.Dom;
+using AngleSharp.Html.Dom;
using Bunit.Rendering;
namespace Bunit;
@@ -255,7 +256,47 @@ public void Test025()
cut.Instance.Invoked.ShouldBeTrue();
}
-
+
+ [Fact(DisplayName = "Find returns element of specified type when it matches")]
+ public void Test026()
+ {
+ var cut = Render(x => x.AddChildContent(""));
+
+ var result = cut.Find("#myInput");
+
+ result.ShouldNotBeNull();
+ result.Type.ShouldBe("text");
+ }
+
+ [Fact(DisplayName = "Find throws ElementNotFoundException when no element matches selector")]
+ public void Test027()
+ {
+ var cut = Render(x => x.AddChildContent(""));
+
+ Should.Throw(() => cut.Find("#nonexistent"));
+ }
+
+ [Fact(DisplayName = "FindAll returns only elements of specified type")]
+ public void Test028()
+ {
+ var cut = Render(x => x.AddChildContent(""));
+
+ var results = cut.FindAll("*");
+
+ results.Count.ShouldBe(2);
+ results.ShouldAllBe(e => e is IHtmlInputElement);
+ }
+
+ [Fact(DisplayName = "FindAll returns empty list when no elements match the type")]
+ public void Test029()
+ {
+ var cut = Render(x => x.AddChildContent(""));
+
+ var results = cut.FindAll("*");
+
+ results.ShouldBeEmpty();
+ }
+
private class BaseComponent : ComponentBase
{
protected override void BuildRenderTree(RenderTreeBuilder builder)
diff --git a/version.json b/version.json
index 13e4cdccc..1c599a00b 100644
--- a/version.json
+++ b/version.json
@@ -1,6 +1,6 @@
{
"$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json",
- "version": "2.2",
+ "version": "2.3",
"assemblyVersion": {
"precision": "revision"
},