@@ -9,8 +9,6 @@ namespace Bunit.Extensions.WaitForHelpers;
99/// </summary>
1010public abstract class WaitForHelper < T > : IDisposable
1111{
12- //// private readonly object lockObject = new();
13- private readonly Timer timer ;
1412 private readonly TaskCompletionSource < T > checkPassedCompletionSource ;
1513 private readonly Func < ( bool CheckPassed , T Content ) > completeChecker ;
1614 private readonly IRenderedFragmentBase renderedFragment ;
@@ -40,60 +38,108 @@ public abstract class WaitForHelper<T> : IDisposable
4038 /// </summary>
4139 public Task < T > WaitTask { get ; }
4240
43-
4441 /// <summary>
4542 /// Initializes a new instance of the <see cref="WaitForHelper{T}"/> class.
4643 /// </summary>
47- protected WaitForHelper ( IRenderedFragmentBase renderedFragment , Func < ( bool CheckPassed , T Content ) > completeChecker , TimeSpan ? timeout = null )
44+ protected WaitForHelper (
45+ IRenderedFragmentBase renderedFragment ,
46+ Func < ( bool CheckPassed , T Content ) > completeChecker ,
47+ TimeSpan ? timeout = null )
4848 {
4949 this . renderedFragment = renderedFragment ?? throw new ArgumentNullException ( nameof ( renderedFragment ) ) ;
5050 this . completeChecker = completeChecker ?? throw new ArgumentNullException ( nameof ( completeChecker ) ) ;
5151 logger = renderedFragment . Services . CreateLogger < WaitForHelper < T > > ( ) ;
52+ checkPassedCompletionSource = new TaskCompletionSource < T > ( ) ;
5253
53- var renderer = renderedFragment . Services . GetRequiredService < ITestRenderer > ( ) ;
54- var renderException = renderer
55- . UnhandledException
56- . ContinueWith ( x => Task . FromException < T > ( x . Result ) , CancellationToken . None , TaskContinuationOptions . OnlyOnRanToCompletion , TaskScheduler . Current )
57- . Unwrap ( ) ;
54+ WaitTask = CreateWaitTask ( renderedFragment , timeout ) ;
5855
59- checkPassedCompletionSource = new TaskCompletionSource < T > ( ) ;
60- WaitTask = Task . WhenAny ( checkPassedCompletionSource . Task , renderException ) . Unwrap ( ) ;
56+ InitializeWaiting ( ) ;
57+ }
58+
59+ /// <summary>
60+ /// Disposes the wait helper and cancels the any ongoing waiting, if it is not
61+ /// already in one of the other completed states.
62+ /// </summary>
63+ public void Dispose ( )
64+ {
65+ Dispose ( disposing : true ) ;
66+ GC . SuppressFinalize ( this ) ;
67+ }
68+
69+ /// <summary>
70+ /// Disposes of the wait task and related logic.
71+ /// </summary>
72+ /// <remarks>
73+ /// The disposing parameter should be false when called from a finalizer, and true when called from the
74+ /// <see cref="Dispose()"/> method. In other words, it is true when deterministically called and false when non-deterministically called.
75+ /// </remarks>
76+ /// <param name="disposing">Set to true if called from <see cref="Dispose()"/>, false if called from a finalizer.f.</param>
77+ protected virtual void Dispose ( bool disposing )
78+ {
79+ if ( isDisposed || ! disposing )
80+ return ;
6181
62- timer = new Timer ( OnTimeout , this , Timeout . InfiniteTimeSpan , Timeout . InfiniteTimeSpan ) ;
82+ isDisposed = true ;
83+ checkPassedCompletionSource . TrySetCanceled ( ) ;
84+ renderedFragment . OnAfterRender -= OnAfterRender ;
85+ logger . LogWaiterDisposed ( renderedFragment . ComponentId ) ;
86+ }
6387
88+ private void InitializeWaiting ( )
89+ {
6490 if ( ! WaitTask . IsCompleted )
6591 {
92+ var renderCountAtSubscribeTime = renderedFragment . RenderCount ;
93+
94+ // Before subscribing to renderedFragment.OnAfterRender,
95+ // we need to make sure that the desired state has not already been reached.
6696 OnAfterRender ( this , EventArgs . Empty ) ;
67- this . renderedFragment . OnAfterRender += OnAfterRender ;
68- OnAfterRender ( this , EventArgs . Empty ) ;
69- StartTimer ( timeout ) ;
97+
98+ SubscribeToOnAfterRender ( ) ;
99+
100+ // If the render count from before subscribing has changes
101+ // till now, we need to do trigger another check, since
102+ // the render may have happened asynchronously and before
103+ // the subscription was set up.
104+ if ( renderCountAtSubscribeTime < renderedFragment . RenderCount )
105+ {
106+ OnAfterRender ( this , EventArgs . Empty ) ;
107+ }
70108 }
71109 }
72110
73- private void StartTimer ( TimeSpan ? timeout )
111+ private Task < T > CreateWaitTask ( IRenderedFragmentBase renderedFragment , TimeSpan ? timeout )
74112 {
75- if ( isDisposed )
76- return ;
113+ var renderer = renderedFragment . Services . GetRequiredService < ITestRenderer > ( ) ;
77114
78- ////lock (lockObject)
79- ////{
80- //// if (isDisposed)
81- //// return;
115+ var renderException = renderer
116+ . UnhandledException
117+ . ContinueWith (
118+ x => Task . FromException < T > ( x . Result ) ,
119+ CancellationToken . None ,
120+ TaskContinuationOptions . OnlyOnRanToCompletion ,
121+ TaskScheduler . Current )
122+ . Unwrap ( ) ;
82123
83- timer . Change ( GetRuntimeTimeout ( timeout ) , Timeout . InfiniteTimeSpan ) ;
84- ////}
124+ var timeoutTask = Task . Delay ( GetRuntimeTimeout ( timeout ) )
125+ . ContinueWith (
126+ x => Task . FromException < T > ( new WaitForFailedException ( TimeoutErrorMessage , capturedException ) ) ,
127+ CancellationToken . None ,
128+ TaskContinuationOptions . OnlyOnRanToCompletion ,
129+ TaskScheduler . Current )
130+ . Unwrap ( ) ;
131+
132+ return Task . WhenAny (
133+ checkPassedCompletionSource . Task ,
134+ renderException ,
135+ timeoutTask ) . Unwrap ( ) ;
85136 }
86137
87138 private void OnAfterRender ( object ? sender , EventArgs args )
88139 {
89140 if ( isDisposed )
90141 return ;
91142
92- ////lock (lockObject)
93- ////{
94- //// if (isDisposed)
95- //// return;
96-
97143 try
98144 {
99145 logger . LogCheckingWaitCondition ( renderedFragment . ComponentId ) ;
@@ -121,61 +167,12 @@ private void OnAfterRender(object? sender, EventArgs args)
121167 Dispose ( ) ;
122168 }
123169 }
124- ////}
125- }
126-
127- private void OnTimeout ( object ? state )
128- {
129- if ( isDisposed )
130- return ;
131-
132- ////lock (lockObject)
133- ////{
134- //// if (isDisposed)
135- //// return;
136-
137- logger . LogWaiterTimedOut ( renderedFragment . ComponentId ) ;
138-
139- checkPassedCompletionSource . TrySetException ( new WaitForFailedException ( TimeoutErrorMessage , capturedException ) ) ;
140-
141- Dispose ( ) ;
142- ////}
143- }
144-
145- /// <summary>
146- /// Disposes the wait helper and cancels the any ongoing waiting, if it is not
147- /// already in one of the other completed states.
148- /// </summary>
149- public void Dispose ( )
150- {
151- Dispose ( disposing : true ) ;
152- GC . SuppressFinalize ( this ) ;
153170 }
154171
155- /// <summary>
156- /// Disposes of the wait task and related logic.
157- /// </summary>
158- /// <remarks>
159- /// The disposing parameter should be false when called from a finalizer, and true when called from the
160- /// <see cref="Dispose()"/> method. In other words, it is true when deterministically called and false when non-deterministically called.
161- /// </remarks>
162- /// <param name="disposing">Set to true if called from <see cref="Dispose()"/>, false if called from a finalizer.f.</param>
163- protected virtual void Dispose ( bool disposing )
172+ private void SubscribeToOnAfterRender ( )
164173 {
165- if ( isDisposed || ! disposing )
166- return ;
167-
168- ////lock (lockObject)
169- ////{
170- //// if (isDisposed)
171- //// return;
172-
173- isDisposed = true ;
174- renderedFragment . OnAfterRender -= OnAfterRender ;
175- timer . Dispose ( ) ;
176- checkPassedCompletionSource . TrySetCanceled ( ) ;
177- logger . LogWaiterDisposed ( renderedFragment . ComponentId ) ;
178- ////}
174+ if ( ! isDisposed && ! WaitTask . IsCompleted )
175+ renderedFragment . OnAfterRender += OnAfterRender ;
179176 }
180177
181178 private static TimeSpan GetRuntimeTimeout ( TimeSpan ? timeout )
0 commit comments