From a0674232628643f076b2c4a37ed9535f18bfd9e7 Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+TheCodeTraveler@users.noreply.github.com> Date: Mon, 13 Oct 2025 09:45:22 -0700 Subject: [PATCH 1/4] Add `ICameraProvider.AvailableCamerasChanged` --- .../Views/CameraView/CameraViewViewModel.cs | 17 ++++++-- .../CameraInfo.shared.cs | 28 ++++++++++--- .../Interfaces/ICameraProvider.shared.cs | 5 +++ .../Providers/CameraProvider.shared.cs | 39 ++++++++++++++++++- 4 files changed, 80 insertions(+), 9 deletions(-) diff --git a/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/CameraView/CameraViewViewModel.cs b/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/CameraView/CameraViewViewModel.cs index 055637f821..53407a2eb8 100644 --- a/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/CameraView/CameraViewViewModel.cs +++ b/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/CameraView/CameraViewViewModel.cs @@ -1,13 +1,19 @@ using CommunityToolkit.Maui.Core; -using CommunityToolkit.Maui.Core.Primitives; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; namespace CommunityToolkit.Maui.Sample.ViewModels.Views; -public partial class CameraViewViewModel(ICameraProvider cameraProvider) : BaseViewModel +public partial class CameraViewViewModel : BaseViewModel { - readonly ICameraProvider cameraProvider = cameraProvider; + readonly ICameraProvider cameraProvider; + + public CameraViewViewModel(ICameraProvider cameraProvider) + { + this.cameraProvider = cameraProvider; + + cameraProvider.AvailableCamerasChanged += HandleAvailableCamerasChanged; + } public IReadOnlyList Cameras => cameraProvider.AvailableCameras ?? []; @@ -78,4 +84,9 @@ void UpdateResolutionText() { ResolutionText = $"Selected Resolution: {SelectedResolution.Width} x {SelectedResolution.Height}"; } + + void HandleAvailableCamerasChanged(object? sender, IReadOnlyList? e) + { + OnPropertyChanged(nameof(Cameras)); + } } \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.Camera/CameraInfo.shared.cs b/src/CommunityToolkit.Maui.Camera/CameraInfo.shared.cs index 13445949cb..8d81a6f8a9 100644 --- a/src/CommunityToolkit.Maui.Camera/CameraInfo.shared.cs +++ b/src/CommunityToolkit.Maui.Camera/CameraInfo.shared.cs @@ -2,6 +2,7 @@ #if IOS || MACCATALYST using AVFoundation; + #elif ANDROID using AndroidX.Camera.Core; #elif WINDOWS @@ -23,18 +24,18 @@ public class CameraInfo( float maximumZoomFactor, IEnumerable supportedResolutions #if ANDROID -, + , CameraSelector cameraSelector #elif IOS || MACCATALYST -, + , AVCaptureDevice captureDevice, IEnumerable supportedFormats #elif WINDOWS -, + , MediaFrameSourceGroup frameSourceGroup, IEnumerable imageEncodingProperties #endif -) +) : IEquatable { /// /// Gets the name of the camera device. @@ -82,9 +83,26 @@ IEnumerable imageEncodingProperties #if WINDOWS internal MediaFrameSourceGroup FrameSourceGroup { get; } = frameSourceGroup; - internal IReadOnlyList ImageEncodingProperties { get; } = [.. imageEncodingProperties]; + internal IReadOnlyList ImageEncodingProperties { get; } = [.. imageEncodingProperties]; #endif + /// + public override bool Equals(object? obj) => Equals(obj as CameraInfo); + + /// + public override int GetHashCode() => HashCode.Combine(Name, DeviceId, Position, IsFlashSupported, MinimumZoomFactor, MaximumZoomFactor, SupportedResolutions); + + /// + public bool Equals(CameraInfo? other) + { + if (other is null) + { + return false; + } + + return DeviceId == other.DeviceId; + } + /// public override string ToString() { diff --git a/src/CommunityToolkit.Maui.Camera/Interfaces/ICameraProvider.shared.cs b/src/CommunityToolkit.Maui.Camera/Interfaces/ICameraProvider.shared.cs index 3f0090e2ec..e1601760bf 100644 --- a/src/CommunityToolkit.Maui.Camera/Interfaces/ICameraProvider.shared.cs +++ b/src/CommunityToolkit.Maui.Camera/Interfaces/ICameraProvider.shared.cs @@ -7,6 +7,11 @@ namespace CommunityToolkit.Maui.Core; /// public interface ICameraProvider { + /// + /// Event fires when the contents has changed + /// + event EventHandler?> AvailableCamerasChanged; + /// /// Cameras available on device /// diff --git a/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.shared.cs b/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.shared.cs index adf24f9846..61f824cf2f 100644 --- a/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.shared.cs +++ b/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.shared.cs @@ -5,9 +5,46 @@ /// partial class CameraProvider : ICameraProvider { + readonly WeakEventManager availableCamerasChangedEventManager = new(); + + public event EventHandler?> AvailableCamerasChanged + { + add => availableCamerasChangedEventManager.AddEventHandler(value); + remove => availableCamerasChangedEventManager.RemoveEventHandler(value); + } + /// - public IReadOnlyList? AvailableCameras { get; private set; } + public IReadOnlyList? AvailableCameras + { + get; + private set + { + if (!AreCameraInfoListsEqual(field, value)) + { + field = value; + availableCamerasChangedEventManager.HandleEvent(this, value, nameof(AvailableCamerasChanged)); + } + } + } /// public partial ValueTask RefreshAvailableCameras(CancellationToken token); + + static bool AreCameraInfoListsEqual(in IReadOnlyList? cameraInfoList1, in IReadOnlyList? cameraInfoList2) + { + if (cameraInfoList1 is null && cameraInfoList2 is null) + { + return true; + } + + if (cameraInfoList1 is null || cameraInfoList2 is null) + { + return false; + } + + var cameraInfosInList1ButNotInList2 = cameraInfoList1.Except(cameraInfoList2).ToList(); + var cameraInfosInList2ButNotInList1 = cameraInfoList2.Except(cameraInfoList1).ToList(); + + return cameraInfosInList1ButNotInList2.Count is 0 && cameraInfosInList2ButNotInList1.Count is 0; + } } \ No newline at end of file From b63d9f0ea01053aca34b509d15276d13786b3a1e Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+TheCodeTraveler@users.noreply.github.com> Date: Mon, 13 Oct 2025 10:10:23 -0700 Subject: [PATCH 2/4] Update MockCameraProvider --- .../Providers/CameraProvider.shared.cs | 2 +- .../Mocks/MockCameraProvider.cs | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.shared.cs b/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.shared.cs index 61f824cf2f..7e7298461d 100644 --- a/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.shared.cs +++ b/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.shared.cs @@ -30,7 +30,7 @@ private set /// public partial ValueTask RefreshAvailableCameras(CancellationToken token); - static bool AreCameraInfoListsEqual(in IReadOnlyList? cameraInfoList1, in IReadOnlyList? cameraInfoList2) + internal static bool AreCameraInfoListsEqual(in IReadOnlyList? cameraInfoList1, in IReadOnlyList? cameraInfoList2) { if (cameraInfoList1 is null && cameraInfoList2 is null) { diff --git a/src/CommunityToolkit.Maui.UnitTests/Mocks/MockCameraProvider.cs b/src/CommunityToolkit.Maui.UnitTests/Mocks/MockCameraProvider.cs index aa42ebeabd..593c4ce70a 100644 --- a/src/CommunityToolkit.Maui.UnitTests/Mocks/MockCameraProvider.cs +++ b/src/CommunityToolkit.Maui.UnitTests/Mocks/MockCameraProvider.cs @@ -5,7 +5,20 @@ namespace CommunityToolkit.Maui.UnitTests.Mocks; public class MockCameraProvider : ICameraProvider { - public IReadOnlyList? AvailableCameras { get; private set; } + public event EventHandler?>? AvailableCamerasChanged; + + public IReadOnlyList? AvailableCameras + { + get; + private set + { + if (!CameraProvider.AreCameraInfoListsEqual(field, value)) + { + field = value; + AvailableCamerasChanged?.Invoke(this, value); + } + } + } public ValueTask RefreshAvailableCameras(CancellationToken token) { From 2b29d5fc2bb90b1d60cdd8e62e1700e5131671ab Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+TheCodeTraveler@users.noreply.github.com> Date: Mon, 13 Oct 2025 10:20:49 -0700 Subject: [PATCH 3/4] Add Unit Tests --- .../Views/CameraView/CameraProviderTests.cs | 636 ++++++++++++++++++ 1 file changed, 636 insertions(+) create mode 100644 src/CommunityToolkit.Maui.UnitTests/Views/CameraView/CameraProviderTests.cs diff --git a/src/CommunityToolkit.Maui.UnitTests/Views/CameraView/CameraProviderTests.cs b/src/CommunityToolkit.Maui.UnitTests/Views/CameraView/CameraProviderTests.cs new file mode 100644 index 0000000000..8300628e01 --- /dev/null +++ b/src/CommunityToolkit.Maui.UnitTests/Views/CameraView/CameraProviderTests.cs @@ -0,0 +1,636 @@ +using CommunityToolkit.Maui.Core; +using CommunityToolkit.Maui.UnitTests.Mocks; +using Xunit; + +namespace CommunityToolkit.Maui.UnitTests; + +/// +/// Comprehensive unit tests for CameraProvider.AreCameraInfoListsEqual method +/// tested through MockCameraProvider behavior +/// +public class CameraProviderTests +{ + #region Null Handling Tests + + [Fact] + public void AreCameraInfoListsEqual_BothListsNull_ShouldConsiderEqual() + { + // Arrange + var provider = new MockCameraProvider(); + var eventRaisedCount = 0; + provider.AvailableCamerasChanged += (s, c) => eventRaisedCount++; + + // Act - Setting null when already null + typeof(MockCameraProvider) + .GetProperty(nameof(MockCameraProvider.AvailableCameras))! + .SetValue(provider, null); + + // Assert - No event should be raised as both are null + Assert.Equal(0, eventRaisedCount); + Assert.Null(provider.AvailableCameras); + } + + [Fact] + public void AreCameraInfoListsEqual_FirstNullSecondNotNull_ShouldConsiderNotEqual() + { + // Arrange + var provider = new MockCameraProvider(); + var eventRaisedCount = 0; + provider.AvailableCamerasChanged += (s, c) => eventRaisedCount++; + + var cameras = new List + { + CreateCameraInfo("Camera1") + }; + + // Act - Setting non-null when currently null + SetAvailableCameras(provider, cameras); + + // Assert - Event should be raised as they're different + Assert.Equal(1, eventRaisedCount); + Assert.NotNull(provider.AvailableCameras); + } + + [Fact] + public void AreCameraInfoListsEqual_FirstNotNullSecondNull_ShouldConsiderNotEqual() + { + // Arrange + var provider = new MockCameraProvider(); + var cameras = new List { CreateCameraInfo("Camera1") }; + SetAvailableCameras(provider, cameras); + + var eventRaisedCount = 0; + provider.AvailableCamerasChanged += (s, c) => eventRaisedCount++; + + // Act - Setting null when currently non-null + SetAvailableCameras(provider, null); + + // Assert - Event should be raised as they're different + Assert.Equal(1, eventRaisedCount); + Assert.Null(provider.AvailableCameras); + } + + #endregion + + #region Empty List Tests + + [Fact] + public void AreCameraInfoListsEqual_BothListsEmpty_ShouldConsiderEqual() + { + // Arrange + var provider = new MockCameraProvider(); + SetAvailableCameras(provider, new List()); + + var eventRaisedCount = 0; + provider.AvailableCamerasChanged += (s, c) => eventRaisedCount++; + + // Act - Setting another empty list + SetAvailableCameras(provider, new List()); + + // Assert - No event should be raised + Assert.Equal(0, eventRaisedCount); + Assert.NotNull(provider.AvailableCameras); + Assert.Empty(provider.AvailableCameras); + } + + [Fact] + public void AreCameraInfoListsEqual_EmptyListVsNull_ShouldConsiderNotEqual() + { + // Arrange + var provider = new MockCameraProvider(); + SetAvailableCameras(provider, new List()); + + var eventRaisedCount = 0; + provider.AvailableCamerasChanged += (s, c) => eventRaisedCount++; + + // Act - Setting null when currently empty + SetAvailableCameras(provider, null); + + // Assert - Event should be raised + Assert.Equal(1, eventRaisedCount); + } + + [Fact] + public void AreCameraInfoListsEqual_EmptyListVsNonEmpty_ShouldConsiderNotEqual() + { + // Arrange + var provider = new MockCameraProvider(); + SetAvailableCameras(provider, new List()); + + var eventRaisedCount = 0; + provider.AvailableCamerasChanged += (s, c) => eventRaisedCount++; + + // Act - Setting non-empty when currently empty + SetAvailableCameras(provider, new List { CreateCameraInfo("Camera1") }); + + // Assert - Event should be raised + Assert.Equal(1, eventRaisedCount); + } + + #endregion + + #region Same Content Tests + + [Fact] + public void AreCameraInfoListsEqual_SameSingleCamera_ShouldConsiderEqual() + { + // Arrange + var camera = CreateCameraInfo("Camera1"); + var provider = new MockCameraProvider(); + SetAvailableCameras(provider, new List { camera }); + + var eventRaisedCount = 0; + provider.AvailableCamerasChanged += (s, c) => eventRaisedCount++; + + // Act - Setting list with same camera + SetAvailableCameras(provider, new List { camera }); + + // Assert - No event should be raised + Assert.Equal(0, eventRaisedCount); + } + + [Fact] + public void AreCameraInfoListsEqual_SameMultipleCameras_ShouldConsiderEqual() + { + // Arrange + var camera1 = CreateCameraInfo("Camera1"); + var camera2 = CreateCameraInfo("Camera2"); + var camera3 = CreateCameraInfo("Camera3"); + + var provider = new MockCameraProvider(); + SetAvailableCameras(provider, new List { camera1, camera2, camera3 }); + + var eventRaisedCount = 0; + provider.AvailableCamerasChanged += (s, c) => eventRaisedCount++; + + // Act - Setting list with same cameras + SetAvailableCameras(provider, new List { camera1, camera2, camera3 }); + + // Assert - No event should be raised + Assert.Equal(0, eventRaisedCount); + } + + [Fact] + public void AreCameraInfoListsEqual_SameCamerasDifferentOrder_ShouldConsiderEqual() + { + // Arrange + var camera1 = CreateCameraInfo("Camera1"); + var camera2 = CreateCameraInfo("Camera2"); + var camera3 = CreateCameraInfo("Camera3"); + + var provider = new MockCameraProvider(); + SetAvailableCameras(provider, new List { camera1, camera2, camera3 }); + + var eventRaisedCount = 0; + provider.AvailableCamerasChanged += (s, c) => eventRaisedCount++; + + // Act - Setting list with same cameras in different order + SetAvailableCameras(provider, new List { camera3, camera1, camera2 }); + + // Assert - No event should be raised (order-independent comparison) + Assert.Equal(0, eventRaisedCount); + } + + [Fact] + public void AreCameraInfoListsEqual_SameReferenceList_ShouldConsiderEqual() + { + // Arrange + var cameras = new List { CreateCameraInfo("Camera1") }; + var provider = new MockCameraProvider(); + SetAvailableCameras(provider, cameras); + + var eventRaisedCount = 0; + provider.AvailableCamerasChanged += (s, c) => eventRaisedCount++; + + // Act - Setting the exact same list reference + SetAvailableCameras(provider, cameras); + + // Assert - No event should be raised + Assert.Equal(0, eventRaisedCount); + } + + #endregion + + #region Different Content Tests + + [Fact] + public void AreCameraInfoListsEqual_DifferentSingleCamera_ShouldConsiderNotEqual() + { + // Arrange + var provider = new MockCameraProvider(); + SetAvailableCameras(provider, new List { CreateCameraInfo("Camera1") }); + + var eventRaisedCount = 0; + provider.AvailableCamerasChanged += (s, c) => eventRaisedCount++; + + // Act - Setting list with different camera + SetAvailableCameras(provider, new List { CreateCameraInfo("Camera2") }); + + // Assert - Event should be raised + Assert.Equal(1, eventRaisedCount); + } + + [Fact] + public void AreCameraInfoListsEqual_DifferentCameraCount_MoreInSecond_ShouldConsiderNotEqual() + { + // Arrange + var camera1 = CreateCameraInfo("Camera1"); + var provider = new MockCameraProvider(); + SetAvailableCameras(provider, new List { camera1 }); + + var eventRaisedCount = 0; + provider.AvailableCamerasChanged += (s, c) => eventRaisedCount++; + + // Act - Setting list with more cameras + SetAvailableCameras(provider, new List { camera1, CreateCameraInfo("Camera2") }); + + // Assert - Event should be raised + Assert.Equal(1, eventRaisedCount); + } + + [Fact] + public void AreCameraInfoListsEqual_DifferentCameraCount_LessInSecond_ShouldConsiderNotEqual() + { + // Arrange + var camera1 = CreateCameraInfo("Camera1"); + var provider = new MockCameraProvider(); + SetAvailableCameras(provider, new List { camera1, CreateCameraInfo("Camera2") }); + + var eventRaisedCount = 0; + provider.AvailableCamerasChanged += (s, c) => eventRaisedCount++; + + // Act - Setting list with fewer cameras + SetAvailableCameras(provider, new List { camera1 }); + + // Assert - Event should be raised + Assert.Equal(1, eventRaisedCount); + } + + [Fact] + public void AreCameraInfoListsEqual_PartiallyOverlappingLists_ShouldConsiderNotEqual() + { + // Arrange + var camera1 = CreateCameraInfo("Camera1"); + var camera2 = CreateCameraInfo("Camera2"); + var camera3 = CreateCameraInfo("Camera3"); + + var provider = new MockCameraProvider(); + SetAvailableCameras(provider, new List { camera1, camera2 }); + + var eventRaisedCount = 0; + provider.AvailableCamerasChanged += (s, c) => eventRaisedCount++; + + // Act - Setting list with one common camera and one different + SetAvailableCameras(provider, new List { camera1, camera3 }); + + // Assert - Event should be raised + Assert.Equal(1, eventRaisedCount); + } + + [Fact] + public void AreCameraInfoListsEqual_CompletelyDifferentLists_ShouldConsiderNotEqual() + { + // Arrange + var provider = new MockCameraProvider(); + SetAvailableCameras(provider, new List + { + CreateCameraInfo("Camera1"), + CreateCameraInfo("Camera2") + }); + + var eventRaisedCount = 0; + provider.AvailableCamerasChanged += (s, c) => eventRaisedCount++; + + // Act - Setting completely different list + SetAvailableCameras(provider, new List + { + CreateCameraInfo("Camera3"), + CreateCameraInfo("Camera4") + }); + + // Assert - Event should be raised + Assert.Equal(1, eventRaisedCount); + } + + #endregion + + #region Duplicate Handling Tests + + [Fact] + public void AreCameraInfoListsEqual_ListWithDuplicates_ShouldHandleCorrectly() + { + // Arrange + var camera = CreateCameraInfo("Camera1"); + var provider = new MockCameraProvider(); + SetAvailableCameras(provider, new List { camera, camera }); + + var eventRaisedCount = 0; + provider.AvailableCamerasChanged += (s, c) => eventRaisedCount++; + + // Act - Setting same list with duplicates + SetAvailableCameras(provider, new List { camera, camera }); + + // Assert - No event should be raised + Assert.Equal(0, eventRaisedCount); + } + + [Fact] + public void AreCameraInfoListsEqual_DifferentNumberOfDuplicates_ShouldConsiderNotEqual() + { + // Arrange + var camera = CreateCameraInfo("Camera1"); + var provider = new MockCameraProvider(); + SetAvailableCameras(provider, new List { camera, camera }); + + var eventRaisedCount = 0; + provider.AvailableCamerasChanged += (s, c) => eventRaisedCount++; + + // Act - Setting list with different number of duplicates + SetAvailableCameras(provider, new List { camera, camera, camera }); + + // Assert - Event should be raised + Assert.Equal(1, eventRaisedCount); + } + + #endregion + + #region CameraInfo Property Variations Tests + + [Fact] + public void AreCameraInfoListsEqual_DifferentCameraName_ShouldConsiderNotEqual() + { + // Arrange + var provider = new MockCameraProvider(); + SetAvailableCameras(provider, new List + { + CreateCameraInfo("Camera1", "device1", CameraPosition.Front) + }); + + var eventRaisedCount = 0; + provider.AvailableCamerasChanged += (s, c) => eventRaisedCount++; + + // Act - Different name, same device and position + SetAvailableCameras(provider, new List + { + CreateCameraInfo("Camera2", "device1", CameraPosition.Front) + }); + + // Assert - Event should be raised (different camera) + Assert.Equal(1, eventRaisedCount); + } + + [Fact] + public void AreCameraInfoListsEqual_DifferentDeviceId_ShouldConsiderNotEqual() + { + // Arrange + var provider = new MockCameraProvider(); + SetAvailableCameras(provider, new List + { + CreateCameraInfo("Camera1", "device1", CameraPosition.Front) + }); + + var eventRaisedCount = 0; + provider.AvailableCamerasChanged += (s, c) => eventRaisedCount++; + + // Act - Same name and position, different device + SetAvailableCameras(provider, new List + { + CreateCameraInfo("Camera1", "device2", CameraPosition.Front) + }); + + // Assert - Event should be raised (different camera) + Assert.Equal(1, eventRaisedCount); + } + + [Fact] + public void AreCameraInfoListsEqual_DifferentPosition_ShouldConsiderNotEqual() + { + // Arrange + var provider = new MockCameraProvider(); + SetAvailableCameras(provider, new List + { + CreateCameraInfo("Camera1", "device1", CameraPosition.Front) + }); + + var eventRaisedCount = 0; + provider.AvailableCamerasChanged += (s, c) => eventRaisedCount++; + + // Act - Same name and device, different position + SetAvailableCameras(provider, new List + { + CreateCameraInfo("Camera1", "device1", CameraPosition.Front) + }); + + // Assert - Event should be raised (different camera) + Assert.Equal(1, eventRaisedCount); + } + + [Fact] + public void AreCameraInfoListsEqual_AllPropertiesSame_ShouldConsiderEqual() + { + // Arrange + var provider = new MockCameraProvider(); + SetAvailableCameras(provider, new List + { + CreateFullCameraInfo("Camera1", "device1", CameraPosition.Front, true, 1.0f, 5.0f) + }); + + var eventRaisedCount = 0; + provider.AvailableCamerasChanged += (s, c) => eventRaisedCount++; + + // Act - All properties identical + SetAvailableCameras(provider, new List + { + CreateFullCameraInfo("Camera1", "device1", CameraPosition.Front, true, 1.0f, 5.0f) + }); + + // Assert - No event should be raised + Assert.Equal(0, eventRaisedCount); + } + + #endregion + + #region Sequential Changes Tests + + [Fact] + public void AreCameraInfoListsEqual_MultipleSequentialChanges_ShouldRaiseEventForEachChange() + { + // Arrange + var provider = new MockCameraProvider(); + var camera1 = CreateCameraInfo("Camera1"); + var camera2 = CreateCameraInfo("Camera2"); + var camera3 = CreateCameraInfo("Camera3"); + + var eventRaisedCount = 0; + provider.AvailableCamerasChanged += (s, c) => eventRaisedCount++; + + // Act - Multiple different changes + SetAvailableCameras(provider, new List { camera1 }); + SetAvailableCameras(provider, new List { camera2 }); + SetAvailableCameras(provider, new List { camera3 }); + SetAvailableCameras(provider, new List { camera1, camera2 }); + + // Assert - Event raised for each change + Assert.Equal(4, eventRaisedCount); + } + + [Fact] + public void AreCameraInfoListsEqual_AlternatingBetweenTwoStates_ShouldRaiseEventForEachChange() + { + // Arrange + var provider = new MockCameraProvider(); + var state1 = new List { CreateCameraInfo("Camera1") }; + var state2 = new List { CreateCameraInfo("Camera2") }; + + var eventRaisedCount = 0; + provider.AvailableCamerasChanged += (s, c) => eventRaisedCount++; + + // Act - Alternating between two different states + SetAvailableCameras(provider, state1); + SetAvailableCameras(provider, state2); + SetAvailableCameras(provider, state1); + SetAvailableCameras(provider, state2); + + // Assert - Event raised for each change + Assert.Equal(4, eventRaisedCount); + } + + [Fact] + public void AreCameraInfoListsEqual_NoChangeBetweenUpdates_ShouldNotRaiseEvent() + { + // Arrange + var camera = CreateCameraInfo("Camera1"); + var provider = new MockCameraProvider(); + SetAvailableCameras(provider, new List { camera }); + + var eventRaisedCount = 0; + provider.AvailableCamerasChanged += (s, c) => eventRaisedCount++; + + // Act - Multiple updates with same content + SetAvailableCameras(provider, new List { camera }); + SetAvailableCameras(provider, new List { camera }); + SetAvailableCameras(provider, new List { camera }); + + // Assert - No events raised + Assert.Equal(0, eventRaisedCount); + } + + #endregion + + #region Large List Tests + + [Fact] + public void AreCameraInfoListsEqual_LargeLists_SameContent_ShouldConsiderEqual() + { + // Arrange + var cameras = Enumerable.Range(0, 100) + .Select(i => CreateCameraInfo($"Camera{i}")) + .ToList(); + + var provider = new MockCameraProvider(); + SetAvailableCameras(provider, cameras); + + var eventRaisedCount = 0; + provider.AvailableCamerasChanged += (s, c) => eventRaisedCount++; + + // Act - Setting same large list + SetAvailableCameras(provider, cameras.ToList()); + + // Assert - No event should be raised + Assert.Equal(0, eventRaisedCount); + } + + [Fact] + public void AreCameraInfoListsEqual_LargeLists_OneDifferent_ShouldConsiderNotEqual() + { + // Arrange + var cameras1 = Enumerable.Range(0, 100) + .Select(i => CreateCameraInfo($"Camera{i}")) + .ToList(); + + var cameras2 = Enumerable.Range(0, 100) + .Select(i => i == 50 ? CreateCameraInfo($"DifferentCamera{i}") : CreateCameraInfo($"Camera{i}")) + .ToList(); + + var provider = new MockCameraProvider(); + SetAvailableCameras(provider, cameras1); + + var eventRaisedCount = 0; + provider.AvailableCamerasChanged += (s, c) => eventRaisedCount++; + + // Act - One camera different in large list + SetAvailableCameras(provider, cameras2); + + // Assert - Event should be raised + Assert.Equal(1, eventRaisedCount); + } + + #endregion + + #region Edge Cases + + [Fact] + public async Task AreCameraInfoListsEqual_AfterRefresh_ShouldCompareCorrectly() + { + // Arrange + var provider = new MockCameraProvider(); + var initialCameras = provider.AvailableCameras; + + // Act - Refresh creates new cameras + await provider.RefreshAvailableCameras(CancellationToken.None); + var firstCamera = provider.AvailableCameras?[0]; + + var eventRaisedCount = 0; + provider.AvailableCamerasChanged += (s, c) => eventRaisedCount++; + + // Refresh again (will create different camera due to new GUID) + await provider.RefreshAvailableCameras(CancellationToken.None); + + // Assert - Event should be raised because GUIDs are different + Assert.Equal(1, eventRaisedCount); + Assert.NotNull(provider.AvailableCameras); + Assert.Single(provider.AvailableCameras); + } + + #endregion + + #region Helper Methods + + static void SetAvailableCameras(MockCameraProvider provider, IReadOnlyList? cameras) + { + var property = typeof(MockCameraProvider).GetProperty(nameof(MockCameraProvider.AvailableCameras)) ?? throw new InvalidOperationException(); + property.SetValue(provider, cameras); + } + + static CameraInfo CreateCameraInfo(string name, string? deviceId = null, CameraPosition position = CameraPosition.Front) + { + return new CameraInfo( + name, + deviceId ?? Guid.NewGuid().ToString(), + position, + false, + 1.0f, + 5.0f, + [new Size(1920, 1080)]); + } + + static CameraInfo CreateFullCameraInfo( + string name, + string deviceId, + CameraPosition position, + bool isSupported, + float minZoom, + float maxZoom) + { + return new CameraInfo( + name, + deviceId, + position, + isSupported, + minZoom, + maxZoom, + [new Size(1920, 1080)]); + } + + #endregion +} \ No newline at end of file From a3b654f6796ff01b13f1898c6dc47a8ddc8edfb7 Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+TheCodeTraveler@users.noreply.github.com> Date: Mon, 13 Oct 2025 10:28:58 -0700 Subject: [PATCH 4/4] Add Unit Tests --- .../CameraInfo.shared.cs | 5 ++- .../Views/CameraView/CameraProviderTests.cs | 45 +------------------ 2 files changed, 6 insertions(+), 44 deletions(-) diff --git a/src/CommunityToolkit.Maui.Camera/CameraInfo.shared.cs b/src/CommunityToolkit.Maui.Camera/CameraInfo.shared.cs index 8d81a6f8a9..1287ee6f7a 100644 --- a/src/CommunityToolkit.Maui.Camera/CameraInfo.shared.cs +++ b/src/CommunityToolkit.Maui.Camera/CameraInfo.shared.cs @@ -87,12 +87,15 @@ IEnumerable imageEncodingProperties #endif /// + /// Equality is determined using public override bool Equals(object? obj) => Equals(obj as CameraInfo); /// - public override int GetHashCode() => HashCode.Combine(Name, DeviceId, Position, IsFlashSupported, MinimumZoomFactor, MaximumZoomFactor, SupportedResolutions); + /// Uses the + public override int GetHashCode() => DeviceId.GetHashCode(); /// + /// Equality is determined using public bool Equals(CameraInfo? other) { if (other is null) diff --git a/src/CommunityToolkit.Maui.UnitTests/Views/CameraView/CameraProviderTests.cs b/src/CommunityToolkit.Maui.UnitTests/Views/CameraView/CameraProviderTests.cs index 8300628e01..5731093af3 100644 --- a/src/CommunityToolkit.Maui.UnitTests/Views/CameraView/CameraProviderTests.cs +++ b/src/CommunityToolkit.Maui.UnitTests/Views/CameraView/CameraProviderTests.cs @@ -334,30 +334,12 @@ public void AreCameraInfoListsEqual_ListWithDuplicates_ShouldHandleCorrectly() Assert.Equal(0, eventRaisedCount); } - [Fact] - public void AreCameraInfoListsEqual_DifferentNumberOfDuplicates_ShouldConsiderNotEqual() - { - // Arrange - var camera = CreateCameraInfo("Camera1"); - var provider = new MockCameraProvider(); - SetAvailableCameras(provider, new List { camera, camera }); - - var eventRaisedCount = 0; - provider.AvailableCamerasChanged += (s, c) => eventRaisedCount++; - - // Act - Setting list with different number of duplicates - SetAvailableCameras(provider, new List { camera, camera, camera }); - - // Assert - Event should be raised - Assert.Equal(1, eventRaisedCount); - } - #endregion #region CameraInfo Property Variations Tests [Fact] - public void AreCameraInfoListsEqual_DifferentCameraName_ShouldConsiderNotEqual() + public void AreCameraInfoListsEqual_DifferentCameraName_ShouldNotConsiderNotEqual() { // Arrange var provider = new MockCameraProvider(); @@ -376,7 +358,7 @@ public void AreCameraInfoListsEqual_DifferentCameraName_ShouldConsiderNotEqual() }); // Assert - Event should be raised (different camera) - Assert.Equal(1, eventRaisedCount); + Assert.Equal(0, eventRaisedCount); } [Fact] @@ -402,29 +384,6 @@ public void AreCameraInfoListsEqual_DifferentDeviceId_ShouldConsiderNotEqual() Assert.Equal(1, eventRaisedCount); } - [Fact] - public void AreCameraInfoListsEqual_DifferentPosition_ShouldConsiderNotEqual() - { - // Arrange - var provider = new MockCameraProvider(); - SetAvailableCameras(provider, new List - { - CreateCameraInfo("Camera1", "device1", CameraPosition.Front) - }); - - var eventRaisedCount = 0; - provider.AvailableCamerasChanged += (s, c) => eventRaisedCount++; - - // Act - Same name and device, different position - SetAvailableCameras(provider, new List - { - CreateCameraInfo("Camera1", "device1", CameraPosition.Front) - }); - - // Assert - Event should be raised (different camera) - Assert.Equal(1, eventRaisedCount); - } - [Fact] public void AreCameraInfoListsEqual_AllPropertiesSame_ShouldConsiderEqual() {