Skip to content

Commit ac76999

Browse files
committed
fix: Wait for dispatcher before starting the timer
1 parent cd04f5d commit ac76999

File tree

1 file changed

+38
-19
lines changed

1 file changed

+38
-19
lines changed

src/bunit/Extensions/WaitForHelpers/WaitForHelper.cs

Lines changed: 38 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)