@@ -8,6 +8,7 @@ namespace Bunit.Rendering;
88/// </summary>
99public class TestRenderer : Renderer , ITestRenderer
1010{
11+ private readonly SynchronizationContext ? usersSyncContext = SynchronizationContext . Current ;
1112 private readonly Dictionary < int , IRenderedFragmentBase > renderedComponents = new ( ) ;
1213 private readonly List < RootComponent > rootComponents = new ( ) ;
1314 private readonly ILogger < TestRenderer > logger ;
@@ -105,7 +106,6 @@ public Task DispatchEventAsync(
105106 }
106107
107108 AssertNoUnhandledExceptions ( ) ;
108-
109109 return result ;
110110 }
111111
@@ -124,7 +124,6 @@ public IReadOnlyList<IRenderedComponentBase<TComponent>> FindComponents<TCompone
124124 where TComponent : IComponent
125125 => FindComponents < TComponent > ( parentComponent , int . MaxValue ) ;
126126
127-
128127 /// <inheritdoc />
129128 public void DisposeComponents ( )
130129 {
@@ -154,10 +153,38 @@ public void DisposeComponents()
154153 /// <inheritdoc/>
155154 protected override Task UpdateDisplayAsync ( in RenderBatch renderBatch )
156155 {
157- logger . LogNewRenderBatchReceived ( ) ;
156+ if ( usersSyncContext is not null && usersSyncContext != SynchronizationContext . Current )
157+ {
158+ // The users' sync context, typically one established by
159+ // xUnit or another testing framework is used to update any
160+ // rendered fragments/dom trees and trigger WaitForX handlers.
161+ // This ensures that changes to DOM observed inside a WaitForX handler
162+ // will also be visible outside a WaitForX handler, since
163+ // they will be running in the same sync context. The theory is that
164+ // this should mitigate the issues where Blazor's dispatcher/thread is used
165+ // to verify an assertion inside a WaitForX handler, and another thread is
166+ // used again to access the DOM/repeat the assertion, where the change
167+ // may not be visible yet (another theory about why that may happen is different
168+ // CPU cache updates not happening immediately).
169+ //
170+ // There is no guarantee a caller/test framework has set a sync context.
171+ usersSyncContext . Send ( static ( state ) =>
172+ {
173+ var ( renderBatch , renderer ) = ( ( RenderBatch , TestRenderer ) ) state ! ;
174+ renderer . UpdateDisplay ( renderBatch ) ;
175+ } , ( renderBatch , this ) ) ;
176+ }
177+ else
178+ {
179+ UpdateDisplay ( renderBatch ) ;
180+ }
158181
159- RenderCount ++ ;
182+ return Task . CompletedTask ;
183+ }
160184
185+ private void UpdateDisplay ( in RenderBatch renderBatch )
186+ {
187+ RenderCount ++ ;
161188 var renderEvent = new RenderEvent ( renderBatch , new RenderTreeFrameDictionary ( ) ) ;
162189
163190 // removes disposed components
@@ -177,12 +204,12 @@ protected override Task UpdateDisplayAsync(in RenderBatch renderBatch)
177204 // notify each rendered component about the render
178205 foreach ( var ( key , rc ) in renderedComponents . ToArray ( ) )
179206 {
180- logger . LogComponentRendered ( rc . ComponentId ) ;
181-
182207 LoadRenderTreeFrames ( rc . ComponentId , renderEvent . Frames ) ;
183208
184209 rc . OnRender ( renderEvent ) ;
185210
211+ logger . LogComponentRendered ( rc . ComponentId ) ;
212+
186213 // RC can replace the instance of the component it is bound
187214 // to while processing the update event.
188215 if ( key != rc . ComponentId )
@@ -191,10 +218,6 @@ protected override Task UpdateDisplayAsync(in RenderBatch renderBatch)
191218 renderedComponents . Add ( rc . ComponentId , rc ) ;
192219 }
193220 }
194-
195- logger . LogChangedComponentsMarkupUpdated ( ) ;
196-
197- return Task . CompletedTask ;
198221 }
199222
200223 /// <inheritdoc/>
@@ -255,45 +278,37 @@ private IReadOnlyList<IRenderedComponentBase<TComponent>> FindComponents<TCompon
255278 if ( parentComponent is null )
256279 throw new ArgumentNullException ( nameof ( parentComponent ) ) ;
257280
258- // Ensure FindComponents runs on the same thread as the renderer,
259- // and that the renderer does not perform any renders while
260- // FindComponents is traversing the current render tree.
261- // Without this, the render tree could change while FindComponentsInternal
262- // is traversing down the render tree, with indeterministic as a results.
263- return Dispatcher . InvokeAsync ( ( ) =>
264- {
265- var result = new List < IRenderedComponentBase < TComponent > > ( ) ;
266- var framesCollection = new RenderTreeFrameDictionary ( ) ;
281+ var result = new List < IRenderedComponentBase < TComponent > > ( ) ;
282+ var framesCollection = new RenderTreeFrameDictionary ( ) ;
267283
268- FindComponentsInRenderTree ( parentComponent . ComponentId ) ;
284+ FindComponentsInRenderTree ( parentComponent . ComponentId ) ;
269285
270- return result ;
286+ return result ;
271287
272- void FindComponentsInRenderTree ( int componentId )
273- {
274- var frames = GetOrLoadRenderTreeFrame ( framesCollection , componentId ) ;
288+ void FindComponentsInRenderTree ( int componentId )
289+ {
290+ var frames = GetOrLoadRenderTreeFrame ( framesCollection , componentId ) ;
275291
276- for ( var i = 0 ; i < frames . Count ; i ++ )
292+ for ( var i = 0 ; i < frames . Count ; i ++ )
293+ {
294+ ref var frame = ref frames . Array [ i ] ;
295+ if ( frame . FrameType == RenderTreeFrameType . Component )
277296 {
278- ref var frame = ref frames . Array [ i ] ;
279- if ( frame . FrameType == RenderTreeFrameType . Component )
297+ if ( frame . Component is TComponent component )
280298 {
281- if ( frame . Component is TComponent component )
282- {
283- result . Add ( GetOrCreateRenderedComponent ( framesCollection , frame . ComponentId , component ) ) ;
284-
285- if ( result . Count == resultLimit )
286- return ;
287- }
288-
289- FindComponentsInRenderTree ( frame . ComponentId ) ;
299+ result . Add ( GetOrCreateRenderedComponent ( framesCollection , frame . ComponentId , component ) ) ;
290300
291301 if ( result . Count == resultLimit )
292302 return ;
293303 }
304+
305+ FindComponentsInRenderTree ( frame . ComponentId ) ;
306+
307+ if ( result . Count == resultLimit )
308+ return ;
294309 }
295310 }
296- } ) . GetAwaiter ( ) . GetResult ( ) ;
311+ }
297312 }
298313
299314 IRenderedComponentBase < TComponent > GetOrCreateRenderedComponent < TComponent > ( RenderTreeFrameDictionary framesCollection , int componentId , TComponent component )
0 commit comments