Skip to content

Commit

Permalink
Fix Unit Tests (#2681)
Browse files Browse the repository at this point in the history
  • Loading branch information
dvoituron authored Sep 18, 2024
1 parent 4cc0e0c commit 916e518
Show file tree
Hide file tree
Showing 6 changed files with 182 additions and 97 deletions.
24 changes: 24 additions & 0 deletions examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml
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

0 comments on commit 916e518

Please sign in to comment.