Skip to content

Commit c90c0a0

Browse files
committed
fix: run markup updates in users sync context
1 parent abb823e commit c90c0a0

2 files changed

Lines changed: 58 additions & 37 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ All notable changes to **bUnit** will be documented in this file. The project ad
66

77
## [Unreleased]
88

9+
10+
### Changed
11+
12+
- Changed test renderer such that updates to rendered components markup happen in the same synchronization context as the test framework is using (if any), if any, to avoid memory race conditions. By [@egil](https://github.com/egil).
13+
- Changed default "WaitFor" timeout to 10 seconds. By [@egil](https://github.com/egil).
14+
915
## [1.18.4] - 2023-02-26
1016

1117
### Fixed

src/bunit.core/Rendering/TestRenderer.cs

Lines changed: 52 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ namespace Bunit.Rendering;
88
/// </summary>
99
public 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

Comments
 (0)