Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Debounce] Fix Unit Tests #2681

Merged
merged 1 commit into from
Sep 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1857,6 +1857,7 @@
ColumnOptions UI. This parameter allows you to enable or disable this resize UI.Enable it by setting the type of resize to perform
Discrete: resize by a 10 pixels at a time
Exact: resize to the exact width specified (in pixels)
Note: This does not affect resizing by mouse dragging, just the keyboard driven resize.
</summary>
</member>
<member name="P:Microsoft.FluentUI.AspNetCore.Components.FluentDataGrid`1.ColumnResizeLabels">
Expand Down Expand Up @@ -2050,6 +2051,29 @@
<member name="M:Microsoft.FluentUI.AspNetCore.Components.FluentDataGrid`1.DisposeAsync">
<inheritdoc />
</member>
<member name="M:Microsoft.FluentUI.AspNetCore.Components.FluentDataGrid`1.SetColumnWidthDiscreteAsync(System.Nullable{System.Int32},System.Single)">
<summary>
Resizes the column width by a discrete amount.
</summary>
<param name="columnIndex">The column to be resized</param>
<param name="widthChange">The amount of pixels to change width with</param>
<returns></returns>
</member>
<member name="M:Microsoft.FluentUI.AspNetCore.Components.FluentDataGrid`1.SetColumnWidthExactAsync(System.Int32,System.Int32)">
<summary>
Resizes the column width to the exact width specified (in pixels).
</summary>
<param name="columnIndex">The column to be resized</param>
<param name="width">The new width in pixels</param>
<returns></returns>
</member>
<member name="M:Microsoft.FluentUI.AspNetCore.Components.FluentDataGrid`1.ResetColumnWidthsAsync">
<summary>
Resets the column widths to their initial values as specified with the <see cref="P:Microsoft.FluentUI.AspNetCore.Components.FluentDataGrid`1.GridTemplateColumns"/> parameter.
If no value is specified, the default value is "1fr" for each column.
</summary>
<returns></returns>
</member>
<member name="P:Microsoft.FluentUI.AspNetCore.Components.FluentDataGridCell`1.Item">
<summary>
Gets or sets the reference to the item that holds this cell's values.
Expand Down
2 changes: 1 addition & 1 deletion src/Core/Utilities/Debounce.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@ namespace Microsoft.FluentUI.AspNetCore.Components.Utilities;
/// The DebounceTask dispatcher delays the invocation of an action until a predetermined interval has elapsed since the last call.
/// This ensures that the action is only invoked once after the calls have stopped for the specified duration.
/// </summary>
public sealed class Debounce : InternalDebounce.DebounceTask
public sealed class Debounce : InternalDebounce.DebounceAction
{
}
25 changes: 19 additions & 6 deletions src/Core/Utilities/InternalDebounce/DebounceAction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ namespace Microsoft.FluentUI.AspNetCore.Components.Utilities.InternalDebounce;
/// The DebounceTask dispatcher delays the invocation of an action until a predetermined interval has elapsed since the last call.
/// This ensures that the action is only invoked once after the calls have stopped for the specified duration.
/// </summary>
[Obsolete("Use Debounce, which inherits from DebounceTask.")]
internal class DebounceAction : IDisposable
public class DebounceAction : IDisposable
{
private bool _disposed;
private readonly System.Timers.Timer _timer = new();
Expand Down Expand Up @@ -54,12 +53,26 @@ public void Run(int milliseconds, Func<Task> action)
/// <param name="milliseconds"></param>
/// <param name="action"></param>
/// <exception cref="ArgumentOutOfRangeException"></exception>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD003:Avoid awaiting foreign Tasks", Justification = "Required to return the current Task.")]
[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "MA0042:Do not use blocking calls in an async method", Justification = "Special case using CurrentTask")]
//[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD003:Avoid awaiting foreign Tasks", Justification = "Required to return the current Task.")]
//[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "MA0042:Do not use blocking calls in an async method", Justification = "Special case using CurrentTask")]
public Task RunAsync(int milliseconds, Func<Task> action)
{
Run(milliseconds, action);
return CurrentTask;
// Check arguments
if (milliseconds <= 0)
{
throw new ArgumentOutOfRangeException(nameof(milliseconds), milliseconds, "The milliseconds must be greater than to zero.");
}

ArgumentNullException.ThrowIfNull(action);

// DebounceTask
if (!_disposed)
{
_taskCompletionSource = _timer.Debounce(action, milliseconds);
return _taskCompletionSource.Task;
}

return Task.CompletedTask;
}

/// <summary>
Expand Down
7 changes: 3 additions & 4 deletions src/Core/Utilities/InternalDebounce/DebounceTask.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ namespace Microsoft.FluentUI.AspNetCore.Components.Utilities.InternalDebounce;
/// The DebounceTask dispatcher delays the invocation of an action until a predetermined interval has elapsed since the last call.
/// This ensures that the action is only invoked once after the calls have stopped for the specified duration.
/// </summary>
public class DebounceTask : IDisposable
[Obsolete("Use Debounce, which inherits from DebounceAction.")]
internal class DebounceTask : IDisposable
{
#if NET9_0_OR_GREATER
private readonly System.Threading.Lock _syncRoot = new();
Expand Down Expand Up @@ -65,7 +66,7 @@ public void Run(int milliseconds, Func<Task> action)
_ = action.Invoke();
}
}
}, _cts.Token, TaskContinuationOptions.OnlyOnRanToCompletion, TaskScheduler.Current);
}, _cts.Token, TaskContinuationOptions.AttachedToParent, TaskScheduler.Default);
}
catch (TaskCanceledException)
{
Expand All @@ -79,8 +80,6 @@ public void Run(int milliseconds, Func<Task> action)
/// <param name="milliseconds"></param>
/// <param name="action"></param>
/// <exception cref="ArgumentOutOfRangeException"></exception>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD003:Avoid awaiting foreign Tasks", Justification = "Required to return the current Task.")]
[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "MA0042:Do not use blocking calls in an async method", Justification = "Special case using CurrentTask")]
public Task RunAsync(int milliseconds, Func<Task> action)
{
Run(milliseconds, action);
Expand Down
106 changes: 65 additions & 41 deletions tests/Core/Utilities/DebounceActionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,25 +19,27 @@ public class DebounceActionTests
public DebounceActionTests(ITestOutputHelper output)
{
Output = output;
Debounce = new DebounceAction();
}

private DebounceAction Debounce { get; init; }

[Fact]
public async Task Debounce_Default()
{
// Arrange
var debounce = new DebounceAction();
var actionCalled = false;
var watcher = Stopwatch.StartNew();

// Act
debounce.Run(50, async () =>
Debounce.Run(50, async () =>
{
actionCalled = true;
await Task.CompletedTask;
});

// Wait for the debounce to complete
await debounce.CurrentTask;
await Debounce.CurrentTask;

// Assert
Assert.True(watcher.ElapsedMilliseconds >= 50);
Expand All @@ -48,27 +50,26 @@ public async Task Debounce_Default()
public async Task Debounce_MultipleCalls()
{
// Arrange
var debounce = new DebounceAction();
var actionCalledCount = 0;
var actionCalled = string.Empty;

// Act
debounce.Run(50, async () =>
Debounce.Run(50, async () =>
{
actionCalled = "Step1";
actionCalledCount++;
await Task.CompletedTask;
});

debounce.Run(40, async () =>
Debounce.Run(40, async () =>
{
actionCalled = "Step2";
actionCalledCount++;
await Task.CompletedTask;
});

// Wait for the debounce to complete
await debounce.CurrentTask;
await Debounce.CurrentTask;

// Assert
Assert.Equal("Step2", actionCalled);
Expand All @@ -78,8 +79,10 @@ public async Task Debounce_MultipleCalls()
[Fact]
public async Task Debounce_MultipleCalls_Async()
{
var watcher = Stopwatch.StartNew();

// Arrange
var debounce = new DebounceAction();
var step1Started = false;
var actionCalledCount = 0;
var actionCalled = string.Empty;
var actionNextCount = 0;
Expand All @@ -88,35 +91,59 @@ public async Task Debounce_MultipleCalls_Async()
// Act: simulate two async calls
var t1 = Task.Run(async () =>
{
step1Started = true;
Output.WriteLine($"{watcher.ElapsedMilliseconds}ms: Start1");
try
{
await debounce.RunAsync(50, async () =>
await Debounce.RunAsync(50, async () =>
{
await Task.Delay(1000); // Let time for the second task to start, and to cancel this one

Output.WriteLine($"{watcher.ElapsedMilliseconds}ms: Step1");
actionCalled = "Step1";
actionCalledCount++;
await Task.CompletedTask;
});

Output.WriteLine($"{watcher.ElapsedMilliseconds}ms: CurrentTask: IsFaulted={Debounce.CurrentTask.IsFaulted} IsCanceled={Debounce.CurrentTask.IsCanceled} IsCompleted={Debounce.CurrentTask.IsCompleted} IsCompletedSuccessfully={Debounce.CurrentTask.IsCompletedSuccessfully}");
if (Debounce.CurrentTask.IsCanceled || Debounce.CurrentTask.IsFaulted)
{
Output.WriteLine($"{watcher.ElapsedMilliseconds}ms: CurrentTask Canceled");
return;
}

Output.WriteLine($"{watcher.ElapsedMilliseconds}ms: Next1");
actionNextCalled = "Next1";
actionNextCount++;
}
catch (TaskCanceledException)
{
Output.WriteLine($"{watcher.ElapsedMilliseconds}ms: Task1 TaskCanceled");
}
catch (OperationCanceledException)
{
// Task cancelled
Output.WriteLine($"{watcher.ElapsedMilliseconds}ms: Task1 OperationCanceled");
}
});

await Task.Delay(15); // Wait for the first task to start
// Wait for Step1 to start.
while (!step1Started)
{
await Task.Delay(10);
}

var t2 = Task.Run(async () =>
{
await debounce.RunAsync(40, async () =>
Output.WriteLine($"{watcher.ElapsedMilliseconds}ms: Start2");
await Debounce.RunAsync(40, async () =>
{
Output.WriteLine($"{watcher.ElapsedMilliseconds}ms: Step2");
actionCalled = "Step2";
actionCalledCount++;
await Task.CompletedTask;
});

Output.WriteLine($"{watcher.ElapsedMilliseconds}ms: Next2");
actionNextCalled = "Next2";
actionNextCount++;
});
Expand All @@ -135,20 +162,19 @@ await debounce.RunAsync(40, async () =>
public async Task Debounce_Disposed()
{
// Arrange
var debounce = new DebounceAction();
var actionCalled = false;

// Act
debounce.Dispose();
Debounce.Dispose();

debounce.Run(50, async () =>
Debounce.Run(50, async () =>
{
actionCalled = true;
await Task.CompletedTask;
});

// Wait for the debounce to complete
await debounce.CurrentTask;
await Debounce.CurrentTask;

// Assert
Assert.False(actionCalled);
Expand All @@ -157,87 +183,85 @@ public async Task Debounce_Disposed()
[Fact]
public async Task Debounce_Busy()
{
// Arrange
var debounce = new DebounceAction();

// Act
debounce.Run(50, async () =>
Debounce.Run(50, async () =>
{
await Task.CompletedTask;
});

// Wait for the debounce to complete
await debounce.CurrentTask;
await Debounce.CurrentTask;

// Assert
Assert.False(debounce.Busy);
Assert.False(Debounce.Busy);
}

[Fact]
public async Task Debounce_Exception()
{
// Arrange
var debounce = new DebounceAction();

// Act
debounce.Run(50, async () =>
Debounce.Run(50, async () =>
{
await Task.CompletedTask;
throw new InvalidProgramException("Error"); // Simulate an exception
});

// Wait for the debounce to complete
await debounce.CurrentTask;
await Debounce.CurrentTask;

// Assert
Assert.False(debounce.Busy);
Assert.False(Debounce.Busy);
}

[Fact]
public void Debounce_DelayMustBePositive()
{
// Arrange
var debounce = new DebounceAction();

// Act
Assert.Throws<ArgumentOutOfRangeException>(() =>
{
debounce.Run(-10, () => Task.CompletedTask);
Debounce.Run(-10, () => Task.CompletedTask);
});
}

[Fact]
public async Task Debounce_FirstRunAlreadyStarted()
{
var watcher = Stopwatch.StartNew();

// Arrange
var debounce = new DebounceAction();
var step1Started = false;
var actionCalledCount = 0;

// Act
debounce.Run(10, async () =>
Debounce.Run(10, async () =>
{
Output.WriteLine("Step1 - Started");
step1Started = true;
Output.WriteLine($"{watcher.ElapsedMilliseconds}ms: Step1 Started");

await Task.Delay(100);
actionCalledCount++;

Output.WriteLine("Step1 - Completed");
Output.WriteLine($"{watcher.ElapsedMilliseconds}ms: Step1 Completed");
});

await Task.Delay(20); // Wait for Step1 to start.
// Wait for Step1 to start.
while (!step1Started)
{
await Task.Delay(10);
}

debounce.Run(10, async () =>
Debounce.Run(10, async () =>
{
Output.WriteLine("Step2 - Started");
Output.WriteLine($"{watcher.ElapsedMilliseconds}ms: Step2 Started");

await Task.CompletedTask;
actionCalledCount++;

Output.WriteLine("Step2 - Completed");
Output.WriteLine($"{watcher.ElapsedMilliseconds}ms: Step2 Completed");
});

// Wait for the debounce to complete
await debounce.CurrentTask;
await Debounce.CurrentTask;
await Task.Delay(200);

// Assert
Expand Down
Loading
Loading