Skip to content

Commit 1be6427

Browse files
committed
WIP: Update tests to check for fixed and not fixed scenario.
1 parent ecdbe6b commit 1be6427

File tree

2 files changed

+289
-82
lines changed

2 files changed

+289
-82
lines changed
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.AspNetCore.Components;
5+
using Microsoft.AspNetCore.Components.QuickGrid;
6+
using Microsoft.JSInterop;
7+
using System.Reflection;
8+
9+
namespace Microsoft.AspNetCore.Components.QuickGrid.Test;
10+
11+
/// <summary>
12+
/// A QuickGrid implementation that simulates the behavior before the race condition fix.
13+
/// This class intentionally does NOT set _disposeBool during disposal to simulate the race condition.
14+
/// </summary>
15+
/// <typeparam name="TGridItem">The type of data represented by each row in the grid.</typeparam>
16+
internal class FailingQuickGrid<TGridItem> : QuickGrid<TGridItem>, IAsyncDisposable
17+
{
18+
[Inject] private IJSRuntime JS { get; set; } = default!;
19+
20+
private readonly TaskCompletionSource _onAfterRenderCompleted = new();
21+
private bool _completionSignaled;
22+
23+
public bool DisposeAsyncWasCalled { get; private set; }
24+
25+
/// <summary>
26+
/// Task that completes when OnAfterRenderAsync has finished executing.
27+
/// This allows tests to wait deterministically for the race condition to occur.
28+
/// </summary>
29+
public Task OnAfterRenderCompleted => _onAfterRenderCompleted.Task;
30+
31+
/// <summary>
32+
/// Intentionally does NOT call base.DisposeAsync() to prevent _disposeBool from being set.
33+
/// This simulates the behavior before the fix was implemented.
34+
/// </summary>
35+
public new async ValueTask DisposeAsync()
36+
{
37+
DisposeAsyncWasCalled = true;
38+
// Intentionally do nothing to prevent _disposeBool from being set to true
39+
// This means the OnAfterRenderAsync method will not detect that the component is disposed
40+
// and will proceed to call init() even after disposal, demonstrating the race condition
41+
42+
// DO NOT call base.DisposeAsync() - this is the key to simulating the race condition
43+
await Task.CompletedTask;
44+
}
45+
46+
/// <summary>
47+
/// Explicit interface implementation to ensure our disposal method is called.
48+
/// </summary>
49+
async ValueTask IAsyncDisposable.DisposeAsync()
50+
{
51+
await DisposeAsync();
52+
}
53+
54+
/// <summary>
55+
/// Check if _disposeBool is false, proving we didn't call base.DisposeAsync().
56+
/// This is used by tests to verify that our simulation is working correctly.
57+
/// </summary>
58+
public bool IsDisposeBoolFalse()
59+
{
60+
var field = typeof(QuickGrid<TGridItem>).GetField("_disposeBool", BindingFlags.NonPublic | BindingFlags.Instance);
61+
return field?.GetValue(this) is false;
62+
}
63+
64+
/// <summary>
65+
/// Override OnAfterRenderAsync to simulate the race condition by NOT checking _disposeBool.
66+
/// This exactly replicates the code path that existed before the race condition fix.
67+
/// </summary>
68+
protected override async Task OnAfterRenderAsync(bool firstRender)
69+
{
70+
try
71+
{
72+
if (firstRender)
73+
{
74+
// Get the IJSRuntime (same as base class)
75+
if (JS != null)
76+
{
77+
// Import the JS module (this will trigger our TestJsRuntime's import logic)
78+
var jsModule = await JS.InvokeAsync<IJSObjectReference>("import",
79+
"./_content/Microsoft.AspNetCore.Components.QuickGrid/QuickGrid.razor.js");
80+
81+
// THE KEY DIFFERENCE: The original code did NOT check _disposeBool here
82+
// The fix added: if (_disposeBool) return;
83+
// By omitting this check, we demonstrate the race condition where init gets called on disposed components
84+
85+
// Call init - this happens even if component was disposed during import
86+
// For our test, we don't need a real table reference, just need to trigger the JS call
87+
await jsModule.InvokeAsync<IJSObjectReference>("init", new object());
88+
89+
// Signal completion only after the init call has completed, and only once
90+
if (!_completionSignaled)
91+
{
92+
_completionSignaled = true;
93+
_onAfterRenderCompleted.TrySetResult();
94+
}
95+
return;
96+
}
97+
}
98+
}
99+
finally
100+
{
101+
// Only signal completion if we haven't already done it and this is the first render
102+
if (firstRender && !_completionSignaled)
103+
{
104+
_completionSignaled = true;
105+
_onAfterRenderCompleted.TrySetResult();
106+
}
107+
}
108+
}
109+
}

0 commit comments

Comments
 (0)