@@ -10,12 +10,12 @@ namespace Bunit.Extensions.WaitForHelpers;
1010/// </summary>
1111public abstract class WaitForHelper < T > : IDisposable
1212{
13- private readonly Timer timer ;
1413 private readonly TaskCompletionSource < T > checkPassedCompletionSource ;
1514 private readonly Func < ( bool CheckPassed , T Content ) > completeChecker ;
1615 private readonly IRenderedFragmentBase renderedFragment ;
1716 private readonly ILogger < WaitForHelper < T > > logger ;
1817 private readonly TestRenderer renderer ;
18+ private readonly Timer ? timer ;
1919 private bool isDisposed ;
2020 private int checkCount ;
2121 private Exception ? capturedException ;
@@ -60,25 +60,37 @@ protected WaitForHelper(
6060 . Renderer ;
6161 checkPassedCompletionSource = new TaskCompletionSource < T > ( TaskCreationOptions . RunContinuationsAsynchronously ) ;
6262
63+ // Create the wait task and run the initial check
64+ // and subscribe to the OnAfterRender event.
65+ // This must happen before the timer is started,
66+ // as the check happens inside the renderers synchronization context,
67+ // and that may be blocked longer than the timeout on overloaded systems,
68+ // resulting in the timer completing before a single check has
69+ // has a chance to complete.
6370 WaitTask = CreateWaitTask ( ) ;
64- timer = new Timer (
65- static ( state ) =>
66- {
67- var @this = ( WaitForHelper < T > ) state ! ;
68- @this . logger . LogWaiterTimedOut ( @this . renderedFragment . ComponentId ) ;
69- @this . checkPassedCompletionSource . TrySetException (
70- new WaitForFailedException (
71- @this . TimeoutErrorMessage ?? string . Empty ,
72- @this . checkCount ,
73- @this . renderedFragment . RenderCount ,
74- @this . renderer . RenderCount ,
75- @this . capturedException ) ) ;
76- } ,
77- this ,
78- GetRuntimeTimeout ( timeout ) ,
79- Timeout . InfiniteTimeSpan ) ;
80-
81- InitializeWaiting ( ) ;
71+ CheckAndInitializeWaiting ( ) ;
72+
73+ // If the initial check did not complete successfully,
74+ // start the timer and recheck after every render until the timer expires.
75+ if ( ! WaitTask . IsCompleted )
76+ {
77+ timer = new Timer (
78+ static ( state ) =>
79+ {
80+ var @this = ( WaitForHelper < T > ) state ! ;
81+ @this . logger . LogWaiterTimedOut ( @this . renderedFragment . ComponentId ) ;
82+ @this . checkPassedCompletionSource . TrySetException (
83+ new WaitForFailedException (
84+ @this . TimeoutErrorMessage ?? string . Empty ,
85+ @this . checkCount ,
86+ @this . renderedFragment . RenderCount ,
87+ @this . renderer . RenderCount ,
88+ @this . capturedException ) ) ;
89+ } ,
90+ this ,
91+ GetRuntimeTimeout ( timeout ) ,
92+ Timeout . InfiniteTimeSpan ) ;
93+ }
8294 }
8395
8496 /// <summary>
@@ -105,13 +117,13 @@ protected virtual void Dispose(bool disposing)
105117 return ;
106118
107119 isDisposed = true ;
108- timer . Dispose ( ) ;
120+ timer ? . Dispose ( ) ;
109121 checkPassedCompletionSource . TrySetCanceled ( ) ;
110122 renderedFragment . OnAfterRender -= OnAfterRender ;
111123 logger . LogWaiterDisposed ( renderedFragment . ComponentId ) ;
112124 }
113125
114- private void InitializeWaiting ( )
126+ private void CheckAndInitializeWaiting ( )
115127 {
116128 if ( ! WaitTask . IsCompleted )
117129 {
0 commit comments