@@ -8,6 +8,8 @@ namespace Bunit.Rendering;
88/// </summary>
99public class TestRenderer : Renderer , ITestRenderer
1010{
11+ private readonly object renderTreeUpdateLock = new ( ) ;
12+ private readonly SynchronizationContext ? usersSyncContext = SynchronizationContext . Current ;
1113 private readonly Dictionary < int , IRenderedFragmentBase > renderedComponents = new ( ) ;
1214 private readonly List < RootComponent > rootComponents = new ( ) ;
1315 private readonly ILogger < TestRenderer > logger ;
@@ -79,34 +81,39 @@ public Task DispatchEventAsync(
7981 if ( fieldInfo is null )
8082 throw new ArgumentNullException ( nameof ( fieldInfo ) ) ;
8183
82- var result = Dispatcher . InvokeAsync ( ( ) =>
84+ // Calling base.DispatchEventAsync updates the render tree
85+ // if the event contains associated data.
86+ lock ( renderTreeUpdateLock )
8387 {
84- ResetUnhandledException ( ) ;
85-
86- try
87- {
88- return base . DispatchEventAsync ( eventHandlerId , fieldInfo , eventArgs ) ;
89- }
90- catch ( ArgumentException ex ) when ( string . Equals ( ex . Message , $ "There is no event handler associated with this event. EventId: '{ eventHandlerId } '. (Parameter 'eventHandlerId')", StringComparison . Ordinal ) )
88+ var result = Dispatcher . InvokeAsync ( ( ) =>
9189 {
92- if ( ignoreUnknownEventHandlers )
90+ ResetUnhandledException ( ) ;
91+
92+ try
93+ {
94+ return base . DispatchEventAsync ( eventHandlerId , fieldInfo , eventArgs ) ;
95+ }
96+ catch ( ArgumentException ex ) when ( string . Equals ( ex . Message , $ "There is no event handler associated with this event. EventId: '{ eventHandlerId } '. (Parameter 'eventHandlerId')", StringComparison . Ordinal ) )
9397 {
94- return Task . CompletedTask ;
98+ if ( ignoreUnknownEventHandlers )
99+ {
100+ return Task . CompletedTask ;
101+ }
102+
103+ var betterExceptionMsg = new UnknownEventHandlerIdException ( eventHandlerId , fieldInfo , ex ) ;
104+ return Task . FromException ( betterExceptionMsg ) ;
95105 }
106+ } ) ;
96107
97- var betterExceptionMsg = new UnknownEventHandlerIdException ( eventHandlerId , fieldInfo , ex ) ;
98- return Task . FromException ( betterExceptionMsg ) ;
108+ if ( result . IsFaulted && result . Exception is not null )
109+ {
110+ HandleException ( result . Exception ) ;
99111 }
100- } ) ;
101112
102- if ( result . IsFaulted && result . Exception is not null )
103- {
104- HandleException ( result . Exception ) ;
105- }
106-
107- AssertNoUnhandledExceptions ( ) ;
113+ AssertNoUnhandledExceptions ( ) ;
108114
109- return result ;
115+ return result ;
116+ }
110117 }
111118
112119 /// <inheritdoc/>
@@ -124,7 +131,6 @@ public IReadOnlyList<IRenderedComponentBase<TComponent>> FindComponents<TCompone
124131 where TComponent : IComponent
125132 => FindComponents < TComponent > ( parentComponent , int . MaxValue ) ;
126133
127-
128134 /// <inheritdoc />
129135 public void DisposeComponents ( )
130136 {
@@ -151,13 +157,54 @@ public void DisposeComponents()
151157 AssertNoUnhandledExceptions ( ) ;
152158 }
153159
160+ /// <inheritdoc/>
161+ protected override void ProcessPendingRender ( )
162+ {
163+ // Blocks updates to the renderers internal render tree
164+ // while the render tree is being read elsewhere.
165+ // base.ProcessPendingRender calls UpdateDisplayAsync,
166+ // so there is no need to lock in that method.
167+ lock ( renderTreeUpdateLock )
168+ {
169+ base . ProcessPendingRender ( ) ;
170+ }
171+ }
172+
154173 /// <inheritdoc/>
155174 protected override Task UpdateDisplayAsync ( in RenderBatch renderBatch )
156175 {
157- logger . LogNewRenderBatchReceived ( ) ;
176+ if ( usersSyncContext is not null && usersSyncContext != SynchronizationContext . Current )
177+ {
178+ // The users' sync context, typically one established by
179+ // xUnit or another testing framework is used to update any
180+ // rendered fragments/dom trees and trigger WaitForX handlers.
181+ // This ensures that changes to DOM observed inside a WaitForX handler
182+ // will also be visible outside a WaitForX handler, since
183+ // they will be running in the same sync context. The theory is that
184+ // this should mitigate the issues where Blazor's dispatcher/thread is used
185+ // to verify an assertion inside a WaitForX handler, and another thread is
186+ // used again to access the DOM/repeat the assertion, where the change
187+ // may not be visible yet (another theory about why that may happen is different
188+ // CPU cache updates not happening immediately).
189+ //
190+ // There is no guarantee a caller/test framework has set a sync context.
191+ usersSyncContext . Send ( static ( state ) =>
192+ {
193+ var ( renderBatch , renderer ) = ( ( RenderBatch , TestRenderer ) ) state ! ;
194+ renderer . UpdateDisplay ( renderBatch ) ;
195+ } , ( renderBatch , this ) ) ;
196+ }
197+ else
198+ {
199+ UpdateDisplay ( renderBatch ) ;
200+ }
201+
202+ return Task . CompletedTask ;
203+ }
158204
205+ private void UpdateDisplay ( in RenderBatch renderBatch )
206+ {
159207 RenderCount ++ ;
160-
161208 var renderEvent = new RenderEvent ( renderBatch , new RenderTreeFrameDictionary ( ) ) ;
162209
163210 // removes disposed components
@@ -177,12 +224,12 @@ protected override Task UpdateDisplayAsync(in RenderBatch renderBatch)
177224 // notify each rendered component about the render
178225 foreach ( var ( key , rc ) in renderedComponents . ToArray ( ) )
179226 {
180- logger . LogComponentRendered ( rc . ComponentId ) ;
181-
182227 LoadRenderTreeFrames ( rc . ComponentId , renderEvent . Frames ) ;
183228
184229 rc . OnRender ( renderEvent ) ;
185230
231+ logger . LogComponentRendered ( rc . ComponentId ) ;
232+
186233 // RC can replace the instance of the component it is bound
187234 // to while processing the update event.
188235 if ( key != rc . ComponentId )
@@ -191,10 +238,6 @@ protected override Task UpdateDisplayAsync(in RenderBatch renderBatch)
191238 renderedComponents . Add ( rc . ComponentId , rc ) ;
192239 }
193240 }
194-
195- logger . LogChangedComponentsMarkupUpdated ( ) ;
196-
197- return Task . CompletedTask ;
198241 }
199242
200243 /// <inheritdoc/>
@@ -255,63 +298,56 @@ private IReadOnlyList<IRenderedComponentBase<TComponent>> FindComponents<TCompon
255298 if ( parentComponent is null )
256299 throw new ArgumentNullException ( nameof ( parentComponent ) ) ;
257300
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 ( ) ;
301+ var result = new List < IRenderedComponentBase < TComponent > > ( ) ;
302+ var framesCollection = new RenderTreeFrameDictionary ( ) ;
267303
304+ // Blocks the renderer from changing the render tree
305+ // while this method searches through it.
306+ lock ( renderTreeUpdateLock )
307+ {
268308 FindComponentsInRenderTree ( parentComponent . ComponentId ) ;
309+ }
269310
270- return result ;
311+ return result ;
271312
272- void FindComponentsInRenderTree ( int componentId )
273- {
274- var frames = GetOrLoadRenderTreeFrame ( framesCollection , componentId ) ;
313+ void FindComponentsInRenderTree ( int componentId )
314+ {
315+ var frames = GetOrLoadRenderTreeFrame ( framesCollection , componentId ) ;
275316
276- for ( var i = 0 ; i < frames . Count ; i ++ )
317+ for ( var i = 0 ; i < frames . Count ; i ++ )
318+ {
319+ ref var frame = ref frames . Array [ i ] ;
320+ if ( frame . FrameType == RenderTreeFrameType . Component )
277321 {
278- ref var frame = ref frames . Array [ i ] ;
279- if ( frame . FrameType == RenderTreeFrameType . Component )
322+ if ( frame . Component is TComponent component )
280323 {
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 ) ;
324+ result . Add ( GetOrCreateRenderedComponent ( framesCollection , frame . ComponentId , component ) ) ;
290325
291326 if ( result . Count == resultLimit )
292327 return ;
293328 }
329+
330+ FindComponentsInRenderTree ( frame . ComponentId ) ;
331+
332+ if ( result . Count == resultLimit )
333+ return ;
294334 }
295335 }
296- } ) . GetAwaiter ( ) . GetResult ( ) ;
336+ }
297337 }
298338
299339 IRenderedComponentBase < TComponent > GetOrCreateRenderedComponent < TComponent > ( RenderTreeFrameDictionary framesCollection , int componentId , TComponent component )
300340 where TComponent : IComponent
301341 {
302- IRenderedComponentBase < TComponent > result ;
303-
304342 if ( renderedComponents . TryGetValue ( componentId , out var renderedComponent ) )
305343 {
306- result = ( IRenderedComponentBase < TComponent > ) renderedComponent ;
307- }
308- else
309- {
310- LoadRenderTreeFrames ( componentId , framesCollection ) ;
311- result = activator . CreateRenderedComponent ( componentId , component , framesCollection ) ;
312- renderedComponents . Add ( result . ComponentId , result ) ;
344+ return ( IRenderedComponentBase < TComponent > ) renderedComponent ;
313345 }
314346
347+ LoadRenderTreeFrames ( componentId , framesCollection ) ;
348+ var result = activator . CreateRenderedComponent ( componentId , component , framesCollection ) ;
349+ renderedComponents . Add ( result . ComponentId , result ) ;
350+
315351 return result ;
316352 }
317353
0 commit comments