Skip to content

Commit 9ecc527

Browse files
authored
feat: Added Async overload of WaitForHelpers (#892)
* feat: Added Async overload of WaitForHelpers
1 parent 4cbc916 commit 9ecc527

File tree

10 files changed

+395
-33
lines changed

10 files changed

+395
-33
lines changed

.github/workflows/verification.yml

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@ on:
1616
- reopened
1717

1818
workflow_dispatch:
19-
19+
2020
concurrency:
2121
group: verification-${{ github.ref }}-1
22-
cancel-in-progress: true
22+
cancel-in-progress: true
2323

2424
jobs:
2525
verify-bunit:
@@ -62,9 +62,12 @@ jobs:
6262
with:
6363
files: '["docs/site/*.md", "docs/**/*.md", "docs/**/*.tmpl.partial", "*.csproj", "**/*.csproj"]'
6464

65-
- name: 🧪 Run unit tests
65+
- name: 🧪 Run unit tests (async)
66+
run: |
67+
dotnet test --filter Category!=sync -c release --blame-hang-timeout 15s --blame-hang-dump-type full --blame-crash-dump-type full
68+
- name: 🧪 Run unit tests (sync)
6669
run: |
67-
dotnet test -c release --blame-hang-timeout 15s --blame-hang-dump-type full --blame-crash-dump-type full
70+
dotnet test --filter Category!=async -c release --blame-hang-timeout 15s --blame-hang-dump-type full --blame-crash-dump-type full
6871
- name: 📛 Upload hang- and crash-dumps on test failure
6972
if: failure()
7073
uses: actions/upload-artifact@v3

src/bunit.core/Extensions/WaitForHelpers/RenderedFragmentWaitForHelperExtensions.cs

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,29 @@ public static void WaitForState(this IRenderedFragmentBase renderedFragment, Fun
3434
{
3535
ExceptionDispatchInfo.Capture(aggregateException.InnerExceptions[0]).Throw();
3636
}
37-
else
38-
{
39-
ExceptionDispatchInfo.Capture(e).Throw();
40-
}
37+
38+
throw;
4139
}
4240
}
4341

42+
/// <summary>
43+
/// Wait until the provided <paramref name="statePredicate"/> action returns true,
44+
/// or the <paramref name="timeout"/> is reached (default is one second).
45+
///
46+
/// The <paramref name="statePredicate"/> is evaluated initially, and then each time
47+
/// the <paramref name="renderedFragment"/> renders.
48+
/// </summary>
49+
/// <param name="renderedFragment">The render fragment or component to attempt to verify state against.</param>
50+
/// <param name="statePredicate">The predicate to invoke after each render, which must returns <c>true</c> when the desired state has been reached.</param>
51+
/// <param name="timeout">The maximum time to wait for the desired state.</param>
52+
/// <exception cref="WaitForFailedException">Thrown if the <paramref name="statePredicate"/> throw an exception during invocation, or if the timeout has been reached. See the inner exception for details.</exception>
53+
internal static async Task WaitForStateAsync(this IRenderedFragmentBase renderedFragment, Func<bool> statePredicate, TimeSpan? timeout = null)
54+
{
55+
using var waiter = new WaitForStateHelper(renderedFragment, statePredicate, timeout);
56+
57+
await waiter.WaitTask;
58+
}
59+
4460
/// <summary>
4561
/// Wait until the provided <paramref name="assertion"/> passes (i.e. does not throw an
4662
/// exception), or the <paramref name="timeout"/> is reached (default is one second).
@@ -66,10 +82,26 @@ public static void WaitForAssertion(this IRenderedFragmentBase renderedFragment,
6682
{
6783
ExceptionDispatchInfo.Capture(aggregateException.InnerExceptions[0]).Throw();
6884
}
69-
else
70-
{
71-
ExceptionDispatchInfo.Capture(e).Throw();
72-
}
85+
86+
throw;
7387
}
7488
}
89+
90+
/// <summary>
91+
/// Wait until the provided <paramref name="assertion"/> passes (i.e. does not throw an
92+
/// exception), or the <paramref name="timeout"/> is reached (default is one second).
93+
///
94+
/// The <paramref name="assertion"/> is attempted initially, and then each time the <paramref name="renderedFragment"/> renders.
95+
/// </summary>
96+
/// <param name="renderedFragment">The rendered fragment to wait for renders from and assert against.</param>
97+
/// <param name="assertion">The verification or assertion to perform.</param>
98+
/// <param name="timeout">The maximum time to attempt the verification.</param>
99+
/// <exception cref="WaitForFailedException">Thrown if the timeout has been reached. See the inner exception to see the captured assertion exception.</exception>
100+
[AssertionMethod]
101+
internal static async Task WaitForAssertionAsync(this IRenderedFragmentBase renderedFragment, Action assertion, TimeSpan? timeout = null)
102+
{
103+
using var waiter = new WaitForAssertionHelper(renderedFragment, assertion, timeout);
104+
105+
await waiter.WaitTask;
106+
}
75107
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Bunit.Core.Tests, PublicKey=002400000480000094000000060200000024000052534131000400000100010001be6b1a2ca57b09b7040e2ab0993e515296ae22aef4031a4fe388a1336fe21f69c7e8610e9935de6ed18d94b5c98429f99ef62ce3d0af28a7088f856239368ea808ad4c448aa2a8075ed581f989f36ed0d0b8b1cfcaf1ff6a4506c8a99b7024b6eb56996d08e3c9c1cf5db59bff96fcc63ccad155ef7fc63aab6a69862437b6")]
2+
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Bunit.Web.Tests, PublicKey=002400000480000094000000060200000024000052534131000400000100010001be6b1a2ca57b09b7040e2ab0993e515296ae22aef4031a4fe388a1336fe21f69c7e8610e9935de6ed18d94b5c98429f99ef62ce3d0af28a7088f856239368ea808ad4c448aa2a8075ed581f989f36ed0d0b8b1cfcaf1ff6a4506c8a99b7024b6eb56996d08e3c9c1cf5db59bff96fcc63ccad155ef7fc63aab6a69862437b6")]

src/bunit.web/Extensions/WaitForHelpers/RenderedFragmentWaitForHelperExtensions.cs

Lines changed: 89 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,77 @@ public static IRefreshableElementCollection<IElement> WaitForElements(this IRend
8080
public static IRefreshableElementCollection<IElement> WaitForElements(this IRenderedFragment renderedFragment, string cssSelector, int matchElementCount, TimeSpan timeout)
8181
=> WaitForElementsCore(renderedFragment, cssSelector, matchElementCount: matchElementCount, timeout: timeout);
8282

83+
/// <summary>
84+
/// Wait until an element matching the <paramref name="cssSelector"/> exists in the <paramref name="renderedFragment"/>,
85+
/// or the timeout is reached (default is one second).
86+
/// </summary>
87+
/// <param name="renderedFragment">The render fragment or component find the matching element in.</param>
88+
/// <param name="cssSelector">The CSS selector to use to search for the element.</param>
89+
/// <exception cref="WaitForFailedException">Thrown if no elements is found matching the <paramref name="cssSelector"/> within the default timeout. See the inner exception for details.</exception>
90+
/// <returns>The <see cref="IElement"/>.</returns>
91+
internal static Task<IElement> WaitForElementAsync(this IRenderedFragment renderedFragment, string cssSelector)
92+
=> WaitForElementCoreAsync(renderedFragment, cssSelector, timeout: null);
93+
94+
/// <summary>
95+
/// Wait until an element matching the <paramref name="cssSelector"/> exists in the <paramref name="renderedFragment"/>,
96+
/// or the <paramref name="timeout"/> is reached.
97+
/// </summary>
98+
/// <param name="renderedFragment">The render fragment or component find the matching element in.</param>
99+
/// <param name="cssSelector">The CSS selector to use to search for the element.</param>
100+
/// <param name="timeout">The maximum time to wait for the element to appear.</param>
101+
/// <exception cref="WaitForFailedException">Thrown if no elements is found matching the <paramref name="cssSelector"/> within the default timeout. See the inner exception for details.</exception>
102+
/// <returns>The <see cref="IElement"/>.</returns>
103+
internal static Task<IElement> WaitForElementAsync(this IRenderedFragment renderedFragment, string cssSelector, TimeSpan timeout)
104+
=> WaitForElementCoreAsync(renderedFragment, cssSelector, timeout: timeout);
105+
106+
/// <summary>
107+
/// Wait until exactly <paramref name="matchElementCount"/> element(s) matching the <paramref name="cssSelector"/> exists in the <paramref name="renderedFragment"/>,
108+
/// or the timeout is reached (default is one second).
109+
/// </summary>
110+
/// <param name="renderedFragment">The render fragment or component find the matching element in.</param>
111+
/// <param name="cssSelector">The CSS selector to use to search for elements.</param>
112+
/// <param name="matchElementCount">The exact number of elements to that the <paramref name="cssSelector"/> should match.</param>
113+
/// <exception cref="WaitForFailedException">Thrown if no elements is found matching the <paramref name="cssSelector"/> within the default timeout.</exception>
114+
/// <returns>The <see cref="IRefreshableElementCollection{IElement}"/>.</returns>
115+
internal static Task<IRefreshableElementCollection<IElement>> WaitForElementsAsync(this IRenderedFragment renderedFragment, string cssSelector, int matchElementCount)
116+
=> WaitForElementsCoreAsync(renderedFragment, cssSelector, matchElementCount: matchElementCount, timeout: null);
117+
118+
/// <summary>
119+
/// Wait until at least one element matching the <paramref name="cssSelector"/> exists in the <paramref name="renderedFragment"/>,
120+
/// or the <paramref name="timeout"/> is reached.
121+
/// </summary>
122+
/// <param name="renderedFragment">The render fragment or component find the matching element in.</param>
123+
/// <param name="cssSelector">The CSS selector to use to search for elements.</param>
124+
/// <param name="timeout">The maximum time to wait for elements to appear.</param>
125+
/// <exception cref="WaitForFailedException">Thrown if no elements is found matching the <paramref name="cssSelector"/> within the default timeout.</exception>
126+
/// <returns>The <see cref="IRefreshableElementCollection{IElement}"/>.</returns>
127+
internal static Task<IRefreshableElementCollection<IElement>> WaitForElementsAsync(this IRenderedFragment renderedFragment, string cssSelector, TimeSpan timeout)
128+
=> WaitForElementsCoreAsync(renderedFragment, cssSelector, matchElementCount: null, timeout: timeout);
129+
130+
/// <summary>
131+
/// Wait until exactly <paramref name="matchElementCount"/> element(s) matching the <paramref name="cssSelector"/> exists in the <paramref name="renderedFragment"/>,
132+
/// or the <paramref name="timeout"/> is reached.
133+
/// </summary>
134+
/// <param name="renderedFragment">The render fragment or component find the matching element in.</param>
135+
/// <param name="cssSelector">The CSS selector to use to search for elements.</param>
136+
/// <param name="matchElementCount">The exact number of elements to that the <paramref name="cssSelector"/> should match.</param>
137+
/// <param name="timeout">The maximum time to wait for elements to appear.</param>
138+
/// <exception cref="WaitForFailedException">Thrown if no elements is found matching the <paramref name="cssSelector"/> within the default timeout.</exception>
139+
/// <returns>The <see cref="IRefreshableElementCollection{IElement}"/>.</returns>
140+
internal static Task<IRefreshableElementCollection<IElement>> WaitForElementsAsync(this IRenderedFragment renderedFragment, string cssSelector, int matchElementCount, TimeSpan timeout)
141+
=> WaitForElementsCoreAsync(renderedFragment, cssSelector, matchElementCount: matchElementCount, timeout: timeout);
142+
143+
/// <summary>
144+
/// Wait until at least one element matching the <paramref name="cssSelector"/> exists in the <paramref name="renderedFragment"/>,
145+
/// or the timeout is reached (default is one second).
146+
/// </summary>
147+
/// <param name="renderedFragment">The render fragment or component find the matching element in.</param>
148+
/// <param name="cssSelector">The CSS selector to use to search for elements.</param>
149+
/// <exception cref="WaitForFailedException">Thrown if no elements is found matching the <paramref name="cssSelector"/> within the default timeout.</exception>
150+
/// <returns>The <see cref="IRefreshableElementCollection{IElement}"/>.</returns>
151+
internal static Task<IRefreshableElementCollection<IElement>> WaitForElementsAsync(this IRenderedFragment renderedFragment, string cssSelector)
152+
=> WaitForElementsCoreAsync(renderedFragment, cssSelector, matchElementCount: null, timeout: null);
153+
83154
private static IElement WaitForElementCore(this IRenderedFragment renderedFragment, string cssSelector, TimeSpan? timeout)
84155
{
85156
using var waiter = new WaitForElementHelper(renderedFragment, cssSelector, timeout);
@@ -94,16 +165,18 @@ private static IElement WaitForElementCore(this IRenderedFragment renderedFragme
94165
{
95166
ExceptionDispatchInfo.Capture(aggregateException.InnerExceptions[0]).Throw();
96167
}
97-
else
98-
{
99-
ExceptionDispatchInfo.Capture(e).Throw();
100-
}
101168

102-
// Unreachable code. Only here because compiler does not know that ExceptionDispatchInfo throws an exception
103169
throw;
104170
}
105171
}
106172

173+
private static async Task<IElement> WaitForElementCoreAsync(this IRenderedFragment renderedFragment, string cssSelector, TimeSpan? timeout)
174+
{
175+
using var waiter = new WaitForElementHelper(renderedFragment, cssSelector, timeout);
176+
177+
return await waiter.WaitTask;
178+
}
179+
107180
private static IRefreshableElementCollection<IElement> WaitForElementsCore(
108181
this IRenderedFragment renderedFragment,
109182
string cssSelector,
@@ -122,13 +195,19 @@ private static IRefreshableElementCollection<IElement> WaitForElementsCore(
122195
{
123196
ExceptionDispatchInfo.Capture(aggregateException.InnerExceptions[0]).Throw();
124197
}
125-
else
126-
{
127-
ExceptionDispatchInfo.Capture(e).Throw();
128-
}
129198

130-
// Unreachable code. Only here because compiler does not know that ExceptionDispatchInfo throws an exception
131199
throw;
132200
}
133201
}
202+
203+
private static async Task<IRefreshableElementCollection<IElement>> WaitForElementsCoreAsync(
204+
this IRenderedFragment renderedFragment,
205+
string cssSelector,
206+
int? matchElementCount,
207+
TimeSpan? timeout)
208+
{
209+
using var waiter = new WaitForElementsHelper(renderedFragment, cssSelector, matchElementCount, timeout);
210+
211+
return await waiter.WaitTask;
212+
}
134213
}

tests/bunit.core.tests/Rendering/TestRendererTest.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -337,7 +337,18 @@ public void Test100()
337337
}
338338

339339
[Fact(DisplayName = "Can render component that awaits yielding task in OnInitializedAsync")]
340-
public void Test101()
340+
[Trait("Category", "async")]
341+
public async Task Test101()
342+
{
343+
var cut = RenderComponent<AsyncRenderOfSubComponentDuringInit>(parameters =>
344+
parameters.Add(p => p.EitherOr, Task.Delay(1)));
345+
346+
await cut.WaitForAssertionAsync(() => cut.Find("h1").TextContent.ShouldBe("SECOND"));
347+
}
348+
349+
[Fact(DisplayName = "Can render component that awaits yielding task in OnInitializedAsync")]
350+
[Trait("Category", "sync")]
351+
public void Test101_Sync()
341352
{
342353
var cut = RenderComponent<AsyncRenderOfSubComponentDuringInit>(parameters =>
343354
parameters.Add(p => p.EitherOr, Task.Delay(1)));

0 commit comments

Comments
 (0)