//
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
//
namespace MUnique.OpenMU.Web.Tests;
using System.Threading;
using MUnique.OpenMU.Web.Shared.Components;
///
/// Tests for .
///
[TestFixture]
public class DebouncerTests
{
///
/// Tests that the constructor throws when delay is zero.
///
[Test]
public void Constructor_WithZeroDelay_ThrowsArgumentOutOfRangeException()
{
Assert.Throws(() => new Debouncer(0));
}
///
/// Tests that the constructor throws when delay is negative.
///
[Test]
public void Constructor_WithNegativeDelay_ThrowsArgumentOutOfRangeException()
{
Assert.Throws(() => new Debouncer(-1));
}
///
/// Tests that a single debounced action executes successfully.
///
[Test]
public async Task DebounceAsync_SingleCall_ExecutesAction()
{
using var debouncer = new Debouncer(50);
var executed = false;
var executionCompleted = new TaskCompletionSource();
await debouncer.DebounceAsync(() =>
{
executed = true;
executionCompleted.TrySetResult(true);
return Task.CompletedTask;
});
await executionCompleted.Task;
Assert.That(executed, Is.True);
}
///
/// Tests that rapid consecutive calls only execute the last action.
///
[Test]
public async Task DebounceAsync_RapidCalls_OnlyLastExecutes()
{
using var debouncer = new Debouncer(100);
var callCount = 0;
var lastValue = string.Empty;
var executionCompleted = new TaskCompletionSource();
// Fire 5 rapid calls
var tasks = new List();
for (var i = 1; i <= 5; i++)
{
var captured = i;
tasks.Add(debouncer.DebounceAsync(() =>
{
Interlocked.Increment(ref callCount);
lastValue = $"call-{captured}";
executionCompleted.TrySetResult(true);
return Task.CompletedTask;
}));
if (i < 5)
{
await Task.Delay(20); // rapid but not instant
}
}
await Task.WhenAll(tasks);
await executionCompleted.Task;
Assert.That(callCount, Is.EqualTo(1));
Assert.That(lastValue, Is.EqualTo("call-5"));
}
///
/// Tests that the cancellation token overload passes the token to the action.
///
[Test]
public async Task DebounceAsync_WithCancellationToken_PassesTokenToAction()
{
using var debouncer = new Debouncer(50);
CancellationToken receivedToken = default;
await debouncer.DebounceAsync(token =>
{
receivedToken = token;
return Task.CompletedTask;
});
Assert.That(receivedToken.IsCancellationRequested, Is.False);
}
///
/// Tests that calling DebounceAsync after disposal returns without executing the action.
///
[Test]
public async Task DebounceAsync_AfterDispose_ReturnsWithoutExecuting()
{
var debouncer = new Debouncer(50);
debouncer.Dispose();
var executed = false;
await debouncer.DebounceAsync(() =>
{
executed = true;
return Task.CompletedTask;
});
Assert.That(executed, Is.False);
}
///
/// Tests that calling Cancel while an action is pending prevents its execution.
///
[Test]
public async Task Cancel_WhilePending_PreventsExecution()
{
using var debouncer = new Debouncer(200);
var executed = false;
var executionCompleted = new TaskCompletionSource();
var task = debouncer.DebounceAsync(() =>
{
executed = true;
executionCompleted.TrySetResult(true);
return Task.CompletedTask;
});
await Task.Delay(50);
debouncer.Cancel();
await task;
Assert.That(executed, Is.False);
Assert.That(executionCompleted.Task.IsCompleted, Is.False, "Execution should not have completed");
}
///
/// Tests that passing a null action throws ArgumentNullException.
///
[Test]
public void DebounceAsync_WithNullAction_ThrowsArgumentNullException()
{
// Arrange
using var debouncer = new Debouncer(50);
// Act & Assert
Assert.That(async () => await debouncer.DebounceAsync((Func)null!), Throws.TypeOf());
}
///
/// Tests that passing a null cancellable action throws ArgumentNullException.
///
[Test]
public void DebounceAsync_WithNullCancellableAction_ThrowsArgumentNullException()
{
// Arrange
using var debouncer = new Debouncer(50);
// Act & Assert
Assert.That(async () => await debouncer.DebounceAsync((Func)null!), Throws.TypeOf());
}
///
/// Tests that actions spaced beyond the debounced window each execute independently.
///
[Test]
public async Task DebounceAsync_SpacedCalls_EachExecutes()
{
using var debouncer = new Debouncer(50);
var callCount = 0;
var executionCompleted = new TaskCompletionSource();
// First call
await debouncer.DebounceAsync(() =>
{
Interlocked.Increment(ref callCount);
executionCompleted.TrySetResult(true);
return Task.CompletedTask;
});
await executionCompleted.Task;
executionCompleted = new TaskCompletionSource();
// Second call - no fixed delay, just let the first complete
await debouncer.DebounceAsync(() =>
{
Interlocked.Increment(ref callCount);
executionCompleted.TrySetResult(true);
return Task.CompletedTask;
});
await executionCompleted.Task;
Assert.That(callCount, Is.EqualTo(2));
}
///
/// Tests that disposing multiple times does not throw an exception.
///
[Test]
public void Dispose_MultipleTimes_DoesNotThrow()
{
var debouncer = new Debouncer(50);
Assert.DoesNotThrow(() =>
{
debouncer.Dispose();
debouncer.Dispose();
});
}
}