Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
bac4e4e
Subscribe to `ModalPopped`
TheCodeTraveler Jun 6, 2026
24eb984
Call `GetParentWindow()` inside `CloseAsync`
TheCodeTraveler Jun 6, 2026
8dc72db
Add Test Timeouts
TheCodeTraveler Jun 6, 2026
2eb2a0f
Update PopupPageTests.cs
TheCodeTraveler Jun 6, 2026
cf29d19
Add `popup.NotifyPopupIsClosed();`
TheCodeTraveler Jun 6, 2026
733dd0d
Update PopupPageTests.cs
TheCodeTraveler Jun 6, 2026
17928b5
Add support for nested Popups
TheCodeTraveler Jun 6, 2026
e8488d6
Add missing unit tests
TheCodeTraveler Jun 6, 2026
ea905c2
Update PopupPage.shared.cs
TheCodeTraveler Jun 6, 2026
6bcc001
Update PopupPageTests.cs
TheCodeTraveler Jun 6, 2026
af0f929
Update PopupPage.shared.cs
TheCodeTraveler Jun 6, 2026
b6f3516
Add PopupPage.ShowAsync
TheCodeTraveler Jun 6, 2026
1b9c1e4
Add Missing Unit Tests
TheCodeTraveler Jun 6, 2026
3b7e949
In `CloseAsync`, implement `navigationSemaphoreSlim` inside its own t…
TheCodeTraveler Jun 6, 2026
866e468
Add missing unit tests
TheCodeTraveler Jun 6, 2026
adf4cc5
Fix null referece
TheCodeTraveler Jun 6, 2026
484c7ce
Update SemaphoreSlim(1,1) logic in CloseAsync
TheCodeTraveler Jun 6, 2026
21d95f9
`dotnet format`
TheCodeTraveler Jun 6, 2026
c40639f
Potential fix for pull request finding
TheCodeTraveler Jun 6, 2026
82d0b00
Potential fix for pull request finding
TheCodeTraveler Jun 6, 2026
1f7f7ec
await SemaphoreSlim(1,1) inside try/finally to ensure event deregistr…
TheCodeTraveler Jun 6, 2026
6b53276
Potential fix for pull request finding
TheCodeTraveler Jun 6, 2026
0f360cc
Update logic for navigationSemaphoreSlim.Release
TheCodeTraveler Jun 6, 2026
030e58b
Merge branch 'Ensure-Popup-Has-Closed-Before-`CloseAsync`-Returns' of…
TheCodeTraveler Jun 6, 2026
1ab81a1
Fix XML error
TheCodeTraveler Jun 6, 2026
dbadc83
Remove `WaitAsync`
TheCodeTraveler Jun 6, 2026
e590829
Potential fix for pull request finding
TheCodeTraveler Jun 6, 2026
5f7c41d
Potential fix for pull request finding
TheCodeTraveler Jun 6, 2026
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 @@ -121,6 +121,58 @@ public async Task ClosePopupT_NullNavigation_ShouldThrowArgumentNullException()
#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type.
}

[Fact]
public void ShowPopup_NullPage_ShouldThrowArgumentNullException()
{
// Arrange / Act / Assert
#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type.
Assert.Throws<ArgumentNullException>(() => PopupExtensions.ShowPopup((Page?)null, new Grid(), PopupOptions.Empty));
#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type.
}

[Fact]
public async Task ShowPopupAsync_NullNavigation_ShouldThrowArgumentNullException()
{
// Arrange / Act / Assert
#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type.
await Assert.ThrowsAsync<ArgumentNullException>(() => PopupExtensions.ShowPopupAsync((INavigation?)null, new Grid(), PopupOptions.Empty, TestContext.Current.CancellationToken));
#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type.
}

[Fact]
public async Task ShowPopupAsync_NullView_ShouldThrowArgumentNullException()
{
// Arrange / Act / Assert
#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type.
await Assert.ThrowsAsync<ArgumentNullException>(() => navigation.ShowPopupAsync((View?)null, PopupOptions.Empty, TestContext.Current.CancellationToken));
#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type.
}

[Fact]
public async Task ShowPopupAsync_Shell_NullShell_ShouldThrowArgumentNullException()
{
// Arrange / Act / Assert
#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type.
await Assert.ThrowsAsync<ArgumentNullException>(() => PopupExtensions.ShowPopupAsync((Shell?)null, new Grid(), PopupOptions.Empty, shellParameters, TestContext.Current.CancellationToken));
#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type.
}

[Fact]
public async Task ShowPopupAsync_Shell_NullView_ShouldThrowArgumentNullException()
{
// Arrange
var shell = new Shell();
shell.Items.Add(new MockPage(new MockPageViewModel()));

Assert.NotNull(Application.Current);
Application.Current.Windows[0].Page = shell;

// Act / Assert
#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type.
await Assert.ThrowsAsync<ArgumentNullException>(() => shell.ShowPopupAsync((View?)null, PopupOptions.Empty, shellParameters, TestContext.Current.CancellationToken));
#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type.
}

[Fact(Timeout = (int)TestDuration.Short)]
public async Task ShowPopupAsync_WithPopupType_ShowsPopupAndClosesPopup()
{
Expand Down Expand Up @@ -1749,6 +1801,22 @@ async Task WaitForModalStackCountAsync(int expectedCount, CancellationToken toke
}
}
}

[Fact(Timeout = (int)TestDuration.Short)]
public async Task ShowPopupAsync_NonNullableValueType_ReturnsDefault_WhenClosedWithoutTypedResult()
{
// Arrange
var showPopupTask = navigation.ShowPopupAsync<int>(new Popup(), PopupOptions.Empty, TestContext.Current.CancellationToken);
var popupPage = (PopupPage)navigation.ModalStack.Last();

// Act
await popupPage.CloseAsync(new PopupResult(false), TestContext.Current.CancellationToken);
var result = await showPopupTask;

// Assert
Assert.False(result.WasDismissedByTappingOutsideOfPopup);
Assert.Equal(default, result.Result);
}
}

sealed class ViewWithIQueryAttributable : Button, IQueryAttributable
Expand Down
132 changes: 131 additions & 1 deletion src/CommunityToolkit.Maui.UnitTests/Services/PopupServiceTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.ComponentModel;
using System.Collections.ObjectModel;
using System.ComponentModel;
using CommunityToolkit.Maui.Services;
using CommunityToolkit.Maui.UnitTests.Mocks;
using CommunityToolkit.Maui.Views;
Expand All @@ -10,6 +11,10 @@ namespace CommunityToolkit.Maui.UnitTests.Services;
public class PopupServiceTests : BaseViewTest
{
readonly INavigation navigation;
readonly ReadOnlyDictionary<string, object> shellParameters = new Dictionary<string, object>
{
[nameof(View.BackgroundColor)] = Colors.Green
}.AsReadOnly();

public PopupServiceTests()
{
Expand Down Expand Up @@ -83,6 +88,77 @@ public void ShowPopupAsync_UsingPage_WithViewType_ShowsPopup()
Assert.IsType<PopupPage>(navigation.ModalStack[0]);
}

[Fact(Timeout = (int)TestDuration.Short)]
public async Task ShowPopup_UsingShell_WithViewType_ShowsPopup()
{
// Arrange
var shell = new Shell();
shell.Items.Add(new MockPage(new MockPageViewModel()));

Assert.NotNull(Application.Current);
Application.Current.Windows[0].Page = shell;

var popupService = ServiceProvider.GetRequiredService<IPopupService>();

// Act
popupService.ShowPopup<ShortLivedSelfClosingPopup>(shell, PopupOptions.Empty, shellParameters);

for (var i = 0; i < 50 && shell.Navigation.ModalStack.Count == 0; i++)
{
await Task.Delay(10, TestContext.Current.CancellationToken);
}

// Assert
Assert.Single(shell.Navigation.ModalStack);
Assert.IsType<PopupPage>(shell.Navigation.ModalStack[0]);

var popupPage = (PopupPage)shell.Navigation.ModalStack[0];
var popup = (ShortLivedSelfClosingPopup)(popupPage.Content.PopupBorder.Content ?? throw new InvalidOperationException("Popup content cannot be null"));
Assert.Equal((Color)shellParameters[nameof(View.BackgroundColor)], popup.BackgroundColor);
}

[Fact(Timeout = (int)TestDuration.Long)]
public async Task ShowPopupAsync_UsingShell_WithViewType_ShowsPopupAndReturnsResult()
{
// Arrange
var shell = new Shell();
shell.Items.Add(new MockPage(new MockPageViewModel()));

Assert.NotNull(Application.Current);
Application.Current.Windows[0].Page = shell;

var popupService = ServiceProvider.GetRequiredService<IPopupService>();

// Act
var result = await popupService.ShowPopupAsync<ShortLivedSelfClosingPopup>(shell, PopupOptions.Empty, shellParameters, TestContext.Current.CancellationToken);

// Assert
Assert.False(result.WasDismissedByTappingOutsideOfPopup);
Assert.Empty(shell.Navigation.ModalStack);
}

[Fact(Timeout = (int)TestDuration.Long)]
public async Task ShowPopupAsyncT_UsingShell_WithViewType_ShowsPopupAndReturnsResult()
{
// Arrange
var shell = new Shell();
shell.Items.Add(new MockPage(new MockPageViewModel()));

Assert.NotNull(Application.Current);
Application.Current.Windows[0].Page = shell;

var popupService = ServiceProvider.GetRequiredService<IPopupService>();
var expectedPopup = ServiceProvider.GetRequiredService<ShortLivedSelfClosingPopup>();

// Act
var result = await popupService.ShowPopupAsync<ShortLivedSelfClosingPopup, object?>(shell, PopupOptions.Empty, shellParameters, TestContext.Current.CancellationToken);

// Assert
Assert.Equal(expectedPopup.Result, result.Result);
Assert.False(result.WasDismissedByTappingOutsideOfPopup);
Assert.Empty(shell.Navigation.ModalStack);
}

[Fact(Timeout = (int)TestDuration.Long)]
public async Task ShowPopupAsync_AwaitingShowPopupAsync_EnsurePreviousPopupClosed()
{
Expand Down Expand Up @@ -322,6 +398,60 @@ public void ShowPopup_UsingPage_ShouldThrowArgumentNullException_WhenNavigationI
#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type.
}

[Fact]
public void ShowPopup_UsingShell_ShouldThrowArgumentNullException_WhenShellIsNull()
{
// Arrange
var popupService = ServiceProvider.GetRequiredService<IPopupService>();

// Act // Assert
#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type.
Assert.Throws<ArgumentNullException>(() => popupService.ShowPopup<ShortLivedSelfClosingPopup>((Shell?)null, PopupOptions.Empty, shellParameters));
#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type.
}

[Fact(Timeout = (int)TestDuration.Short)]
public async Task ShowPopupAsync_UsingShell_ShouldThrowArgumentNullException_WhenShellIsNull()
{
// Arrange
var popupService = ServiceProvider.GetRequiredService<IPopupService>();

// Act // Assert
#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type.
await Assert.ThrowsAsync<ArgumentNullException>(() => popupService.ShowPopupAsync<ShortLivedSelfClosingPopup>((Shell?)null, PopupOptions.Empty, shellParameters, TestContext.Current.CancellationToken));
#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type.
}

[Fact(Timeout = (int)TestDuration.Short)]
public async Task ShowPopupAsyncT_UsingShell_ShouldThrowArgumentNullException_WhenShellIsNull()
{
// Arrange
var popupService = ServiceProvider.GetRequiredService<IPopupService>();

// Act // Assert
#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type.
await Assert.ThrowsAsync<ArgumentNullException>(() => popupService.ShowPopupAsync<ShortLivedSelfClosingPopup, object?>((Shell?)null, PopupOptions.Empty, shellParameters, TestContext.Current.CancellationToken));
#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type.
}

[Fact(Timeout = (int)TestDuration.Short)]
public async Task ShowPopupAsync_UsingShell_ShouldThrowOperationCanceledException_WhenTokenCanceled()
{
// Arrange
var shell = new Shell();
shell.Items.Add(new MockPage(new MockPageViewModel()));

Assert.NotNull(Application.Current);
Application.Current.Windows[0].Page = shell;

var popupService = ServiceProvider.GetRequiredService<IPopupService>();
var cts = new CancellationTokenSource();
await cts.CancelAsync();

// Act / Assert
await Assert.ThrowsAsync<OperationCanceledException>(() => popupService.ShowPopupAsync<ShortLivedSelfClosingPopup>(shell, PopupOptions.Empty, shellParameters, cts.Token));
}

[Fact(Timeout = (int)TestDuration.Short)]
public async Task ClosePopupAsync_UsingPage_ShouldThrowArgumentNullException_WhenPageIsNull()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,59 @@ public DefaultPopupOptionsSettingsTests()
navigation = page.Navigation;
}

[Fact]
public void DefaultPopupOptionsSettings_DefaultConstructor_UsesExpectedDefaults()
{
// Arrange
IPopupOptions defaults = new DefaultPopupOptionsSettings();

// Assert
Assert.True(defaults.CanBeDismissedByTappingOutsideOfPopup);
Assert.Null(defaults.OnTappingOutsideOfPopup);
Assert.Equal(Colors.Black.WithAlpha(0.3f), defaults.PageOverlayColor);
Assert.Equal(Colors.LightGray, ((RoundRectangle?)defaults.Shape)?.Stroke);
Assert.Equal(2, ((RoundRectangle?)defaults.Shape)?.StrokeThickness);
Assert.Equal(Colors.Black, defaults.Shadow?.Brush);
}

[Fact]
public void DefaultPopupOptionsSettings_WithOverrides_UsesProvidedValues()
{
// Arrange
bool actionInvoked = false;
var expectedShape = new Ellipse
{
Stroke = Colors.Red,
StrokeThickness = 8
};
var expectedShadow = new Shadow
{
Brush = Colors.Blue,
Offset = new Point(1, 2),
Radius = 3,
Opacity = 0.4f
};

IPopupOptions options = new DefaultPopupOptionsSettings
{
CanBeDismissedByTappingOutsideOfPopup = false,
OnTappingOutsideOfPopup = () => actionInvoked = true,
PageOverlayColor = Colors.Green,
Shape = expectedShape,
Shadow = expectedShadow
};

// Act
options.OnTappingOutsideOfPopup?.Invoke();

// Assert
Assert.False(options.CanBeDismissedByTappingOutsideOfPopup);
Assert.True(actionInvoked);
Assert.Equal(Colors.Green, options.PageOverlayColor);
Assert.Same(expectedShape, options.Shape);
Assert.Same(expectedShadow, options.Shadow);
}

[Fact]
public void Popup_SetPopupOptionsDefaultsNotCalled_UsesPopupOptionsDefaults()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,44 @@ namespace CommunityToolkit.Maui.UnitTests.Views;

public class DefaultPopupSettingsTests : BaseViewTest
{
[Fact]
public void DefaultPopupSettings_DefaultConstructor_UsesExpectedDefaults()
{
// Arrange
var settings = new DefaultPopupSettings();

// Assert
Assert.True(settings.CanBeDismissedByTappingOutsideOfPopup);
Assert.Equal(new Thickness(30), settings.Margin);
Assert.Equal(new Thickness(15), settings.Padding);
Assert.Equal(LayoutOptions.Center, settings.HorizontalOptions);
Assert.Equal(LayoutOptions.Center, settings.VerticalOptions);
Assert.Equal(Colors.White, settings.BackgroundColor);
}

[Fact]
public void DefaultPopupSettings_WithOverrides_UsesProvidedValues()
{
// Arrange
var settings = new DefaultPopupSettings
{
CanBeDismissedByTappingOutsideOfPopup = false,
BackgroundColor = Colors.Orange,
HorizontalOptions = LayoutOptions.End,
VerticalOptions = LayoutOptions.Start,
Margin = 72,
Padding = 4
};

// Assert
Assert.False(settings.CanBeDismissedByTappingOutsideOfPopup);
Assert.Equal(Colors.Orange, settings.BackgroundColor);
Assert.Equal(LayoutOptions.End, settings.HorizontalOptions);
Assert.Equal(LayoutOptions.Start, settings.VerticalOptions);
Assert.Equal(new Thickness(72), settings.Margin);
Assert.Equal(new Thickness(4), settings.Padding);
}

[Fact]
public void Popup_SetPopupDefaultsNotCalled_UsesPopupDefaults()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,42 @@ public void HorizontalOptions_ShouldReturnCorrectLayoutOptions()
Assert.Equal(horizontalOptions, options.HorizontalOptions);
}

[Fact]
public void Empty_ShouldReturnSameInstance_AndExposeDefaultValues()
{
// Arrange
var emptyA = PopupOptions.Empty;
var emptyB = PopupOptions.Empty;

// Assert
Assert.Same(emptyA, emptyB);
Assert.True(emptyA.CanBeDismissedByTappingOutsideOfPopup);
Assert.Equal(Colors.Black.WithAlpha(0.3f), emptyA.PageOverlayColor);
Assert.Null(emptyA.OnTappingOutsideOfPopup);
Assert.Equal(Colors.LightGray, ((RoundRectangle?)emptyA.Shape)?.Stroke);
}

[Fact]
public void PopupOptions_ShouldImplementIPopupOptionsContract()
{
// Arrange
IPopupOptions options = new PopupOptions
{
CanBeDismissedByTappingOutsideOfPopup = false,
PageOverlayColor = Colors.Purple,
OnTappingOutsideOfPopup = null,
Shape = null,
Shadow = null
};

// Assert
Assert.False(options.CanBeDismissedByTappingOutsideOfPopup);
Assert.Equal(Colors.Purple, options.PageOverlayColor);
Assert.Null(options.OnTappingOutsideOfPopup);
Assert.Null(options.Shape);
Assert.Null(options.Shadow);
}

sealed class MockPopupOptions : IPopupOptions
{
public bool CanBeDismissedByTappingOutsideOfPopup { get; set; }
Expand Down
Loading
Loading