Skip to content
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
17 changes: 17 additions & 0 deletions src/Core/src/Animations/PlatformTicker.Windows.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,32 @@ namespace Microsoft.Maui.Animations
/// <inheritdoc/>
public class PlatformTicker : Ticker
{
bool _isRunning;

/// <inheritdoc/>
public override bool IsRunning => _isRunning;

/// <inheritdoc/>
public override void Start()
{
if (_isRunning)
{
return;
}

_isRunning = true;
CompositionTarget.Rendering += RenderingFrameEventHandler;
}

/// <inheritdoc/>
public override void Stop()
{
if (!_isRunning)
{
return;
}

_isRunning = false;
CompositionTarget.Rendering -= RenderingFrameEventHandler;
}

Expand Down
1 change: 1 addition & 0 deletions src/Core/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#nullable enable
override Microsoft.Maui.Animations.PlatformTicker.IsRunning.get -> bool
override Microsoft.Maui.Platform.LayoutPanel.OnCreateAutomationPeer() -> Microsoft.UI.Xaml.Automation.Peers.AutomationPeer!
override Microsoft.Maui.Platform.MauiPasswordTextBox.OnCreateAutomationPeer() -> Microsoft.UI.Xaml.Automation.Peers.AutomationPeer!
override Microsoft.Maui.Platform.ContentPanel.MeasureOverride(Windows.Foundation.Size availableSize) -> Windows.Foundation.Size
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
using System.Threading.Tasks;
using Microsoft.Maui.Animations;
using Xunit;

namespace Microsoft.Maui.DeviceTests
{
[Category(TestCategory.Animation)]
public class PlatformTickerTests : TestBase
{
[Fact]
public async Task IsRunning_ReflectsStartAndStop()
{
await InvokeOnMainThreadAsync(() =>
{
var ticker = new PlatformTicker();

Assert.False(ticker.IsRunning, "Should be false before Start");

ticker.Start();
Assert.True(ticker.IsRunning, "Should be true after Start");

ticker.Stop();
Assert.False(ticker.IsRunning, "Should be false after Stop");
});
}

[Fact]
public async Task Start_IsIdempotent_NoDuplicateSubscriptions()
{
await InvokeOnMainThreadAsync(async () =>
{
var ticker = new PlatformTicker();
int fireCount = 0;
ticker.Fire = () => fireCount++;

ticker.Start();
ticker.Start(); // second call should be a no-op
ticker.Start(); // third call should be a no-op

// Yield a few composition frames
await Task.Delay(100);

ticker.Stop();
var firesAfterStop = fireCount;

await Task.Delay(50);

// No additional fires after Stop
Assert.Equal(firesAfterStop, fireCount);
// ~6 expected at 60Hz over 100ms; 3x subscriptions would yield ~18+
Assert.InRange(firesAfterStop, 1, 20);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[major] Regression Prevention and Test Coverage — This assertion does not actually catch the duplicate-subscription regression. With the pre-fix implementation, three Start() calls can produce roughly 18 callbacks over 100ms, which still passes the current upper bound of 20. That means the test can pass while Start() is not idempotent. Tighten this to prove duplicate subscriptions are not occurring, for example by comparing against a single-start baseline or using a deterministic frame signal rather than a broad timing range.

});
}

[Fact]
public async Task AnimationManager_StartsTickerOnce_AcrossMultipleAdds()
{
await InvokeOnMainThreadAsync(() =>
{
var ticker = new PlatformTicker();
var manager = new AnimationManager(ticker);

for (int i = 0; i < 5; i++)
manager.Add(new Animation { Duration = 1.0 });

Assert.True(ticker.IsRunning);

ticker.Stop();
Assert.False(ticker.IsRunning);
});
}
}
}
1 change: 1 addition & 0 deletions src/Core/tests/DeviceTests/TestCategory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,5 +51,6 @@ public static class TestCategory
public const string View = "View";
public const string WebView = "WebView";
public const string Window = "Window";
public const string Animation = "Animation";
}
}
Loading