@@ -69,28 +69,45 @@ protected WaitForHelper(
6969 // resulting in the timer completing before a single check has
7070 // has a chance to complete.
7171 WaitTask = CreateWaitTask ( ) ;
72- CheckAndInitializeWaiting ( ) ;
72+ var initializationTask = CheckAndInitializeWaiting ( ) ;
7373
7474 // If the initial check did not complete successfully,
7575 // start the timer and recheck after every render until the timer expires.
76+ // Wait for the initialization to complete to ensure the OnAfterRender
77+ // subscription is registered before the timer can expire.
78+ // This prevents a race condition where the timer fires before renders
79+ // are being monitored, causing WaitForState to timeout prematurely.
7680 if ( ! WaitTask . IsCompleted )
7781 {
78- timer = new Timer (
79- static ( state ) =>
80- {
81- var @this = ( WaitForHelper < T , TComponent > ) state ! ;
82- @this . logger . LogWaiterTimedOut ( @this . renderedComponent . ComponentId ) ;
83- @this . checkPassedCompletionSource . TrySetException (
84- new WaitForFailedException (
85- @this . TimeoutErrorMessage ?? string . Empty ,
86- @this . checkCount ,
87- @this . renderedComponent . RenderCount ,
88- @this . renderer . RenderCount ,
89- @this . capturedException ) ) ;
90- } ,
91- this ,
92- GetRuntimeTimeout ( timeout ) ,
93- Timeout . InfiniteTimeSpan ) ;
82+ // Ensure subscription is complete before starting the timer.
83+ // On slower systems, the InvokeAsync queue may be delayed,
84+ // and we need to ensure OnAfterRender is subscribed before
85+ // the timeout timer starts ticking.
86+ if ( ! initializationTask . IsCompleted )
87+ {
88+ initializationTask . GetAwaiter ( ) . GetResult ( ) ;
89+ }
90+
91+ // Only start the timer if we still haven't completed after initialization
92+ if ( ! WaitTask . IsCompleted )
93+ {
94+ timer = new Timer (
95+ static ( state ) =>
96+ {
97+ var @this = ( WaitForHelper < T , TComponent > ) state ! ;
98+ @this . logger . LogWaiterTimedOut ( @this . renderedComponent . ComponentId ) ;
99+ @this . checkPassedCompletionSource . TrySetException (
100+ new WaitForFailedException (
101+ @this . TimeoutErrorMessage ?? string . Empty ,
102+ @this . checkCount ,
103+ @this . renderedComponent . RenderCount ,
104+ @this . renderer . RenderCount ,
105+ @this . capturedException ) ) ;
106+ } ,
107+ this ,
108+ GetRuntimeTimeout ( timeout ) ,
109+ Timeout . InfiniteTimeSpan ) ;
110+ }
94111 }
95112 }
96113
@@ -124,7 +141,7 @@ protected virtual void Dispose(bool disposing)
124141 logger . LogWaiterDisposed ( renderedComponent . ComponentId ) ;
125142 }
126143
127- private void CheckAndInitializeWaiting ( )
144+ private Task CheckAndInitializeWaiting ( )
128145 {
129146 if ( ! WaitTask . IsCompleted )
130147 {
@@ -134,14 +151,16 @@ private void CheckAndInitializeWaiting()
134151 // This also ensures that checks performed during OnAfterRender,
135152 // which are usually not atomic, e.g. search the DOM tree,
136153 // can be performed without the DOM tree changing.
137- renderedComponent . InvokeAsync ( ( ) =>
154+ return renderedComponent . InvokeAsync ( ( ) =>
138155 {
139156 // Before subscribing to renderedFragment.OnAfterRender,
140157 // we need to make sure that the desired state has not already been reached.
141158 OnAfterRender ( this , EventArgs . Empty ) ;
142159 SubscribeToOnAfterRender ( ) ;
143160 } ) ;
144161 }
162+
163+ return Task . CompletedTask ;
145164 }
146165
147166 private Task < T > CreateWaitTask ( )
0 commit comments