[dev-v5] Add Debounce (#2656)
* Add new Debounce class * Add Unit Tests * Fix Unit Test * Add Debounce Timer * Refactore names * Fix Task MultipleCalls * Refactoring to create a common class Debounce
This commit is contained in:
Родитель
3c21b67522
Коммит
f68fb1e54a
|
@ -0,0 +1,13 @@
|
|||
// ------------------------------------------------------------------------
|
||||
// MIT License - Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
namespace Microsoft.FluentUI.AspNetCore.Components.Utilities;
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
{
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
// ------------------------------------------------------------------------
|
||||
// MIT License - Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
namespace Microsoft.FluentUI.AspNetCore.Components.Utilities.InternalDebounce;
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
{
|
||||
private bool _disposed;
|
||||
private readonly System.Timers.Timer _timer = new();
|
||||
private TaskCompletionSource? _taskCompletionSource;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the DebounceTask dispatcher is busy.
|
||||
/// </summary>
|
||||
public bool Busy => _taskCompletionSource?.Task.Status == TaskStatus.Running && !_disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current task.
|
||||
/// </summary>
|
||||
public Task CurrentTask => _taskCompletionSource?.Task ?? Task.CompletedTask;
|
||||
|
||||
/// <summary>
|
||||
/// Delays the invocation of an action until a predetermined interval has elapsed since the last call.
|
||||
/// </summary>
|
||||
/// <param name="milliseconds"></param>
|
||||
/// <param name="action"></param>
|
||||
/// <exception cref="ArgumentOutOfRangeException"></exception>
|
||||
public void Run(int milliseconds, Func<Task> action)
|
||||
{
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases all resources used by the DebounceTask dispatcher.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_taskCompletionSource = null;
|
||||
_timer.Dispose();
|
||||
_disposed = true;
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
// ------------------------------------------------------------------------
|
||||
// MIT License - Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
namespace Microsoft.FluentUI.AspNetCore.Components.Utilities.InternalDebounce;
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
{
|
||||
#if NET9_0_OR_GREATER
|
||||
private readonly System.Threading.Lock _syncRoot = new();
|
||||
#else
|
||||
private readonly object _syncRoot = new();
|
||||
#endif
|
||||
|
||||
private bool _disposed;
|
||||
private Task? _task;
|
||||
private CancellationTokenSource? _cts;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the DebounceTask dispatcher is busy.
|
||||
/// </summary>
|
||||
public bool Busy => _task?.Status == TaskStatus.Running && !_disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current task.
|
||||
/// </summary>
|
||||
public Task CurrentTask => _task ?? Task.CompletedTask;
|
||||
|
||||
/// <summary>
|
||||
/// Delays the invocation of an action until a predetermined interval has elapsed since the last call.
|
||||
/// </summary>
|
||||
/// <param name="milliseconds"></param>
|
||||
/// <param name="action"></param>
|
||||
/// <exception cref="ArgumentOutOfRangeException"></exception>
|
||||
public void Run(int milliseconds, Func<Task> action)
|
||||
{
|
||||
// Check arguments
|
||||
if (milliseconds <= 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(milliseconds), milliseconds, "The milliseconds must be greater than to zero.");
|
||||
}
|
||||
|
||||
ArgumentNullException.ThrowIfNull(action);
|
||||
|
||||
// Cancel the previous task if it's still running
|
||||
_cts?.Cancel();
|
||||
|
||||
// Create a new cancellation token source
|
||||
_cts = new CancellationTokenSource();
|
||||
|
||||
try
|
||||
{
|
||||
// Wait for the specified time
|
||||
_task = Task.Delay(TimeSpan.FromMilliseconds(milliseconds), _cts.Token)
|
||||
.ContinueWith(t =>
|
||||
{
|
||||
if (!_disposed && !_cts.IsCancellationRequested)
|
||||
{
|
||||
lock (_syncRoot)
|
||||
{
|
||||
_ = action.Invoke();
|
||||
}
|
||||
}
|
||||
}, _cts.Token, TaskContinuationOptions.OnlyOnRanToCompletion, TaskScheduler.Current);
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
// Task was canceled
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases all resources used by the DebounceTask dispatcher.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_disposed = true;
|
||||
_cts?.Cancel();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
// ------------------------------------------------------------------------
|
||||
// MIT License - Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace Microsoft.FluentUI.AspNetCore.Components.Utilities.InternalDebounce;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for <see cref="System.Timers.Timer"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Inspired from Microsoft.Toolkit.Uwp.UI.DispatcherQueueTimerExtensions
|
||||
/// </remarks>
|
||||
internal static class DispatcherTimerExtensions
|
||||
{
|
||||
private static readonly ConcurrentDictionary<System.Timers.Timer, TimerDebounceItem> _debounceInstances = new();
|
||||
|
||||
/// <summary>
|
||||
/// Delays the invocation of an action until a predetermined interval has elapsed since the last call.
|
||||
/// </summary>
|
||||
/// <param name="timer"></param>
|
||||
/// <param name="action"></param>
|
||||
/// <param name="interval"></param>
|
||||
/// <returns></returns>
|
||||
public static TaskCompletionSource Debounce(this System.Timers.Timer timer, Func<Task> action, double interval)
|
||||
{
|
||||
// Check and stop any existing timer
|
||||
timer.Stop();
|
||||
|
||||
// Reset timer parameters
|
||||
timer.Elapsed -= Timer_Elapsed;
|
||||
timer.Interval = interval;
|
||||
|
||||
// If we're not in immediate mode, then we'll execute when the current timer expires.
|
||||
timer.Elapsed += Timer_Elapsed;
|
||||
|
||||
// Store/Update function
|
||||
TimerDebounceItem updateValueFactory(System.Timers.Timer k, TimerDebounceItem v) => v.UpdateAction(action);
|
||||
var item = _debounceInstances.AddOrUpdate(
|
||||
key: timer,
|
||||
addValue: new TimerDebounceItem()
|
||||
{
|
||||
Status = new TaskCompletionSource(),
|
||||
Action = action,
|
||||
},
|
||||
updateValueFactory: updateValueFactory);
|
||||
|
||||
// Start the timer to keep track of the last call here.
|
||||
timer.Start();
|
||||
|
||||
return item.Status;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Timer elapsed event handler.
|
||||
/// </summary>
|
||||
/// <param name="sender"></param>
|
||||
/// <param name="e"></param>
|
||||
private static void Timer_Elapsed(object? sender, System.Timers.ElapsedEventArgs e)
|
||||
{
|
||||
// This event is only registered/run if we weren't in immediate mode above
|
||||
if (sender is System.Timers.Timer timer)
|
||||
{
|
||||
timer.Elapsed -= Timer_Elapsed;
|
||||
timer.Stop();
|
||||
|
||||
if (_debounceInstances.TryRemove(timer, out var item))
|
||||
{
|
||||
_ = (item?.Action.Invoke());
|
||||
item?.Status.SetResult();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Timer debounce item.
|
||||
/// </summary>
|
||||
private class TimerDebounceItem
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the task completion source.
|
||||
/// </summary>
|
||||
public required TaskCompletionSource Status { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the action to execute.
|
||||
/// </summary>
|
||||
public required Func<Task> Action { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Updates the action to execute.
|
||||
/// </summary>
|
||||
/// <param name="action"></param>
|
||||
/// <returns></returns>
|
||||
public TimerDebounceItem UpdateAction(Func<Task> action)
|
||||
{
|
||||
Action = action;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,236 @@
|
|||
// ------------------------------------------------------------------------
|
||||
// MIT License - Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Bunit;
|
||||
using Microsoft.FluentUI.AspNetCore.Components.Utilities;
|
||||
using Microsoft.FluentUI.AspNetCore.Components.Utilities.InternalDebounce;
|
||||
using Microsoft.VisualStudio.TestPlatform.Utilities;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Microsoft.FluentUI.AspNetCore.Components.Tests.Utilities;
|
||||
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
|
||||
public class DebounceActionTests
|
||||
{
|
||||
private readonly ITestOutputHelper Output;
|
||||
|
||||
public DebounceActionTests(ITestOutputHelper output)
|
||||
{
|
||||
Output = output;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Debounce_Default()
|
||||
{
|
||||
// Arrange
|
||||
var debounce = new DebounceAction();
|
||||
var actionCalled = false;
|
||||
var watcher = Stopwatch.StartNew();
|
||||
|
||||
// Act
|
||||
debounce.Run(50, async () =>
|
||||
{
|
||||
actionCalled = true;
|
||||
await Task.CompletedTask;
|
||||
});
|
||||
|
||||
// Wait for the debounce to complete
|
||||
await debounce.CurrentTask;
|
||||
|
||||
// Assert
|
||||
Assert.True(watcher.ElapsedMilliseconds >= 50);
|
||||
Assert.True(actionCalled);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Debounce_MultipleCalls()
|
||||
{
|
||||
// Arrange
|
||||
var debounce = new DebounceAction();
|
||||
var actionCalledCount = 0;
|
||||
var actionCalled = string.Empty;
|
||||
|
||||
// Act
|
||||
debounce.Run(50, async () =>
|
||||
{
|
||||
actionCalled = "Step1";
|
||||
actionCalledCount++;
|
||||
await Task.CompletedTask;
|
||||
});
|
||||
|
||||
debounce.Run(40, async () =>
|
||||
{
|
||||
actionCalled = "Step2";
|
||||
actionCalledCount++;
|
||||
await Task.CompletedTask;
|
||||
});
|
||||
|
||||
// Wait for the debounce to complete
|
||||
await debounce.CurrentTask;
|
||||
|
||||
// Assert
|
||||
Assert.Equal("Step2", actionCalled);
|
||||
Assert.Equal(1, actionCalledCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Debounce_MultipleCalls_Async()
|
||||
{
|
||||
// Arrange
|
||||
var debounce = new DebounceAction();
|
||||
var actionCalledCount = 0;
|
||||
var actionCalled = string.Empty;
|
||||
|
||||
// Act: simulate two async calls
|
||||
_ = Task.Run(() =>
|
||||
{
|
||||
debounce.Run(50, async () =>
|
||||
{
|
||||
actionCalled = "Step1";
|
||||
actionCalledCount++;
|
||||
await Task.CompletedTask;
|
||||
});
|
||||
});
|
||||
|
||||
await Task.Delay(5); // To ensure the second call is made after the first one
|
||||
|
||||
_ = Task.Run(() =>
|
||||
{
|
||||
debounce.Run(40, async () =>
|
||||
{
|
||||
actionCalled = "Step2";
|
||||
actionCalledCount++;
|
||||
await Task.CompletedTask;
|
||||
});
|
||||
});
|
||||
|
||||
await Task.Delay(100); // Wait for the debounce to complete
|
||||
|
||||
// Assert
|
||||
Assert.Equal("Step2", actionCalled);
|
||||
Assert.Equal(1, actionCalledCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Debounce_Disposed()
|
||||
{
|
||||
// Arrange
|
||||
var debounce = new DebounceAction();
|
||||
var actionCalled = false;
|
||||
|
||||
// Act
|
||||
debounce.Dispose();
|
||||
|
||||
debounce.Run(50, async () =>
|
||||
{
|
||||
actionCalled = true;
|
||||
await Task.CompletedTask;
|
||||
});
|
||||
|
||||
// Wait for the debounce to complete
|
||||
await debounce.CurrentTask;
|
||||
|
||||
// Assert
|
||||
Assert.False(actionCalled);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Debounce_Busy()
|
||||
{
|
||||
// Arrange
|
||||
var debounce = new DebounceAction();
|
||||
|
||||
// Act
|
||||
debounce.Run(50, async () =>
|
||||
{
|
||||
await Task.CompletedTask;
|
||||
});
|
||||
|
||||
// Wait for the debounce to complete
|
||||
await debounce.CurrentTask;
|
||||
|
||||
// Assert
|
||||
Assert.False(debounce.Busy);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Debounce_Exception()
|
||||
{
|
||||
// Arrange
|
||||
var debounce = new DebounceAction();
|
||||
|
||||
// Act
|
||||
debounce.Run(50, async () =>
|
||||
{
|
||||
await Task.CompletedTask;
|
||||
throw new InvalidProgramException("Error"); // Simulate an exception
|
||||
});
|
||||
|
||||
// Wait for the debounce to complete
|
||||
await debounce.CurrentTask;
|
||||
|
||||
// Assert
|
||||
Assert.False(debounce.Busy);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Debounce_DelayMustBePositive()
|
||||
{
|
||||
// Arrange
|
||||
var debounce = new DebounceAction();
|
||||
|
||||
// Act
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() =>
|
||||
{
|
||||
debounce.Run(-10, () => Task.CompletedTask);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Debounce_FirstRunAlreadyStarted()
|
||||
{
|
||||
// Arrange
|
||||
var debounce = new DebounceAction();
|
||||
var actionCalledCount = 0;
|
||||
|
||||
// Act
|
||||
debounce.Run(10, async () =>
|
||||
{
|
||||
Output.WriteLine("Step1 - Started");
|
||||
|
||||
await Task.Delay(100);
|
||||
actionCalledCount++;
|
||||
|
||||
Output.WriteLine("Step1 - Completed");
|
||||
});
|
||||
|
||||
await Task.Delay(20); // Wait for Step1 to start.
|
||||
|
||||
debounce.Run(10, async () =>
|
||||
{
|
||||
Output.WriteLine("Step2 - Started");
|
||||
|
||||
await Task.CompletedTask;
|
||||
actionCalledCount++;
|
||||
|
||||
Output.WriteLine("Step2 - Completed");
|
||||
});
|
||||
|
||||
// Wait for the debounce to complete
|
||||
await debounce.CurrentTask;
|
||||
await Task.Delay(200);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, actionCalledCount);
|
||||
}
|
||||
}
|
||||
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
|
@ -0,0 +1,232 @@
|
|||
// ------------------------------------------------------------------------
|
||||
// MIT License - Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Bunit;
|
||||
using Microsoft.FluentUI.AspNetCore.Components.Utilities;
|
||||
using Microsoft.FluentUI.AspNetCore.Components.Utilities.InternalDebounce;
|
||||
using Microsoft.VisualStudio.TestPlatform.Utilities;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Microsoft.FluentUI.AspNetCore.Components.Tests.Utilities;
|
||||
|
||||
public class DebounceTaskTests
|
||||
{
|
||||
private readonly ITestOutputHelper Output;
|
||||
|
||||
public DebounceTaskTests(ITestOutputHelper output)
|
||||
{
|
||||
Output = output;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Debounce_Default()
|
||||
{
|
||||
// Arrange
|
||||
var debounce = new DebounceTask();
|
||||
var actionCalled = false;
|
||||
var watcher = Stopwatch.StartNew();
|
||||
|
||||
// Act
|
||||
debounce.Run(50, async () =>
|
||||
{
|
||||
actionCalled = true;
|
||||
await Task.CompletedTask;
|
||||
});
|
||||
|
||||
// Wait for the debounce to complete
|
||||
await debounce.CurrentTask;
|
||||
|
||||
// Assert
|
||||
Assert.True(watcher.ElapsedMilliseconds >= 50);
|
||||
Assert.True(actionCalled);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Debounce_MultipleCalls()
|
||||
{
|
||||
// Arrange
|
||||
var debounce = new DebounceTask();
|
||||
var actionCalledCount = 0;
|
||||
var actionCalled = string.Empty;
|
||||
|
||||
// Act
|
||||
debounce.Run(50, async () =>
|
||||
{
|
||||
actionCalled = "Step1";
|
||||
actionCalledCount++;
|
||||
await Task.CompletedTask;
|
||||
});
|
||||
|
||||
debounce.Run(40, async () =>
|
||||
{
|
||||
actionCalled = "Step2";
|
||||
actionCalledCount++;
|
||||
await Task.CompletedTask;
|
||||
});
|
||||
|
||||
// Wait for the debounce to complete
|
||||
await debounce.CurrentTask;
|
||||
|
||||
// Assert
|
||||
Assert.Equal("Step2", actionCalled);
|
||||
Assert.Equal(1, actionCalledCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Debounce_MultipleCalls_Async()
|
||||
{
|
||||
// Arrange
|
||||
var debounce = new DebounceTask();
|
||||
var actionCalledCount = 0;
|
||||
var actionCalled = string.Empty;
|
||||
|
||||
// Act: simulate two async calls
|
||||
_ = Task.Run(() =>
|
||||
{
|
||||
debounce.Run(50, async () =>
|
||||
{
|
||||
actionCalled = "Step1";
|
||||
actionCalledCount++;
|
||||
await Task.CompletedTask;
|
||||
});
|
||||
});
|
||||
|
||||
await Task.Delay(5); // To ensure the second call is made after the first one
|
||||
|
||||
_ = Task.Run(() =>
|
||||
{
|
||||
debounce.Run(40, async () =>
|
||||
{
|
||||
actionCalled = "Step2";
|
||||
actionCalledCount++;
|
||||
await Task.CompletedTask;
|
||||
});
|
||||
});
|
||||
|
||||
await Task.Delay(100); // Wait for the debounce to complete
|
||||
|
||||
// Assert
|
||||
Assert.Equal("Step2", actionCalled);
|
||||
Assert.Equal(1, actionCalledCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Debounce_Disposed()
|
||||
{
|
||||
// Arrange
|
||||
var debounce = new DebounceTask();
|
||||
var actionCalled = false;
|
||||
|
||||
// Act
|
||||
debounce.Dispose();
|
||||
|
||||
debounce.Run(50, async () =>
|
||||
{
|
||||
actionCalled = true;
|
||||
await Task.CompletedTask;
|
||||
});
|
||||
|
||||
// Wait for the debounce to complete
|
||||
await debounce.CurrentTask;
|
||||
|
||||
// Assert
|
||||
Assert.False(actionCalled);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Debounce_Busy()
|
||||
{
|
||||
// Arrange
|
||||
var debounce = new DebounceTask();
|
||||
|
||||
// Act
|
||||
debounce.Run(50, async () =>
|
||||
{
|
||||
await Task.CompletedTask;
|
||||
});
|
||||
|
||||
// Wait for the debounce to complete
|
||||
await debounce.CurrentTask;
|
||||
|
||||
// Assert
|
||||
Assert.False(debounce.Busy);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Debounce_Exception()
|
||||
{
|
||||
// Arrange
|
||||
var debounce = new DebounceTask();
|
||||
|
||||
// Act
|
||||
debounce.Run(50, async () =>
|
||||
{
|
||||
await Task.CompletedTask;
|
||||
throw new InvalidProgramException("Error"); // Simulate an exception
|
||||
});
|
||||
|
||||
// Wait for the debounce to complete
|
||||
await debounce.CurrentTask;
|
||||
|
||||
// Assert
|
||||
Assert.False(debounce.Busy);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Debounce_DelayMustBePositive()
|
||||
{
|
||||
// Arrange
|
||||
var debounce = new DebounceTask();
|
||||
|
||||
// Act
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() =>
|
||||
{
|
||||
debounce.Run(-10, () => Task.CompletedTask);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Debounce_FirstRunAlreadyStarted()
|
||||
{
|
||||
// Arrange
|
||||
var debounce = new DebounceTask();
|
||||
var actionCalledCount = 0;
|
||||
|
||||
// Act
|
||||
debounce.Run(10, async () =>
|
||||
{
|
||||
Output.WriteLine("Step1 - Started");
|
||||
|
||||
await Task.Delay(100);
|
||||
actionCalledCount++;
|
||||
|
||||
Output.WriteLine("Step1 - Completed");
|
||||
});
|
||||
|
||||
await Task.Delay(20); // Wait for Step1 to start.
|
||||
|
||||
debounce.Run(10, async () =>
|
||||
{
|
||||
Output.WriteLine("Step2 - Started");
|
||||
|
||||
await Task.CompletedTask;
|
||||
actionCalledCount++;
|
||||
|
||||
Output.WriteLine("Step2 - Completed");
|
||||
});
|
||||
|
||||
// Wait for the debounce to complete
|
||||
await debounce.CurrentTask;
|
||||
await Task.Delay(200);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, actionCalledCount);
|
||||
}
|
||||
}
|
Загрузка…
Ссылка в новой задаче