From 2eaa05106fd418dae9a7bedf5c3f1187bf332e74 Mon Sep 17 00:00:00 2001 From: Matthew Leibowitz Date: Fri, 26 Jan 2024 02:51:33 +0200 Subject: [PATCH] Enable Windows Image device tests --- .../HandlerTests/HandlerTestBasementOfT.cs | 25 +++++ .../tests/DeviceTests/Core.DeviceTests.csproj | 1 - .../Image/ImageHandlerTests.Windows.cs | 26 +++++ .../Handlers/Image/ImageHandlerTests.cs | 80 ++++++++++++---- .../BaseImageSourceServiceTests.Windows.cs | 94 +++++++++++++++++++ .../Stubs/CountedImageHandler.Windows.cs | 22 +++++ .../AssertionExtensions.Windows.cs | 7 ++ 7 files changed, 235 insertions(+), 20 deletions(-) create mode 100644 src/Core/tests/DeviceTests/Handlers/Image/ImageHandlerTests.Windows.cs create mode 100644 src/Core/tests/DeviceTests/Services/ImageSource/BaseImageSourceServiceTests.Windows.cs create mode 100644 src/Core/tests/DeviceTests/Stubs/CountedImageHandler.Windows.cs diff --git a/src/Core/tests/DeviceTests.Shared/HandlerTests/HandlerTestBasementOfT.cs b/src/Core/tests/DeviceTests.Shared/HandlerTests/HandlerTestBasementOfT.cs index 4b086cfb2c77..6cacb006725b 100644 --- a/src/Core/tests/DeviceTests.Shared/HandlerTests/HandlerTestBasementOfT.cs +++ b/src/Core/tests/DeviceTests.Shared/HandlerTests/HandlerTestBasementOfT.cs @@ -3,6 +3,17 @@ using Microsoft.Maui.DeviceTests.Stubs; using Xunit; using Xunit.Sdk; +#if __IOS__ || MACCATALYST +using PlatformView = UIKit.UIView; +#elif __ANDROID__ +using PlatformView = Android.Views.View; +#elif WINDOWS +using PlatformView = Microsoft.UI.Xaml.FrameworkElement; +#elif TIZEN +using PlatformView = Tizen.NUI.BaseComponents.View; +#elif (NETSTANDARD || !PLATFORM) +using PlatformView = System.Object; +#endif namespace Microsoft.Maui.DeviceTests { @@ -45,6 +56,20 @@ public Task AttachAndRun(IView view, Func> action) }, MauiContext, async (view) => (IPlatformViewHandler)(await CreateHandlerAsync(view))); } + public Task AttachAndRun(PlatformView view, Action action) => +#if WINDOWS + view.AttachAndRun(action, MauiContext); +#else + view.AttachAndRun(action); +#endif + + public Task AttachAndRun(PlatformView view, Func action) => +#if WINDOWS + view.AttachAndRun(action, MauiContext); +#else + view.AttachAndRun(action); +#endif + protected Task CreateHandlerAsync(IView view) { return InvokeOnMainThreadAsync(() => diff --git a/src/Core/tests/DeviceTests/Core.DeviceTests.csproj b/src/Core/tests/DeviceTests/Core.DeviceTests.csproj index 44dfa9df3180..8d29aa881402 100644 --- a/src/Core/tests/DeviceTests/Core.DeviceTests.csproj +++ b/src/Core/tests/DeviceTests/Core.DeviceTests.csproj @@ -55,7 +55,6 @@ - diff --git a/src/Core/tests/DeviceTests/Handlers/Image/ImageHandlerTests.Windows.cs b/src/Core/tests/DeviceTests/Handlers/Image/ImageHandlerTests.Windows.cs new file mode 100644 index 000000000000..76990f5d39b6 --- /dev/null +++ b/src/Core/tests/DeviceTests/Handlers/Image/ImageHandlerTests.Windows.cs @@ -0,0 +1,26 @@ +using System; +using Microsoft.UI.Xaml.Media.Imaging; +using WImage = Microsoft.UI.Xaml.Controls.Image; +using WStretch = Microsoft.UI.Xaml.Media.Stretch; + +namespace Microsoft.Maui.DeviceTests +{ + public partial class ImageHandlerTests + { + WImage GetPlatformImageView(IImageHandler imageHandler) => + imageHandler.PlatformView; + + bool GetNativeIsAnimationPlaying(IImageHandler imageHandler) => + GetPlatformImageView(imageHandler).Source is BitmapImage bitmapImage && bitmapImage.IsPlaying; + + Aspect GetNativeAspect(IImageHandler imageHandler) => + GetPlatformImageView(imageHandler).Stretch switch + { + WStretch.Uniform => Aspect.AspectFit, + WStretch.UniformToFill => Aspect.AspectFill, + WStretch.Fill => Aspect.Fill, + WStretch.None => Aspect.Center, + _ => throw new ArgumentOutOfRangeException("Stretch") + }; + } +} \ No newline at end of file diff --git a/src/Core/tests/DeviceTests/Handlers/Image/ImageHandlerTests.cs b/src/Core/tests/DeviceTests/Handlers/Image/ImageHandlerTests.cs index dabce0ffa659..eac95b449577 100644 --- a/src/Core/tests/DeviceTests/Handlers/Image/ImageHandlerTests.cs +++ b/src/Core/tests/DeviceTests/Handlers/Image/ImageHandlerTests.cs @@ -13,6 +13,8 @@ #elif IOS || MACCATALYST using UIKit; using PlatformImageType = UIKit.UIImage; +#elif WINDOWS +using PlatformImageType = Microsoft.UI.Xaml.Controls.Image; #endif namespace Microsoft.Maui.DeviceTests @@ -32,11 +34,14 @@ public abstract partial class ImageHandlerTests : CoreHand #elif IOS || MACCATALYST const string ImageEventAppResourceMemberName = "Image"; const string ImageEventCustomMemberName = "Image"; +#elif WINDOWS + const string ImageEventAppResourceMemberName = "Source"; + const string ImageEventCustomMemberName = "Source"; #endif [Theory( -#if IOS || MACCATALYST - Skip = "Test failing on iOS" +#if IOS || MACCATALYST || WINDOWS + Skip = "Test failing on iOS and WINDOWS" #endif )] [InlineData("#FF0000")] @@ -55,28 +60,28 @@ await InvokeOnMainThreadAsync(async () => var handler = CreateHandler(image); var platformView = GetPlatformImageView(handler); - await platformView.AttachAndRun(async () => + await AttachAndRun(platformView, async () => { // the first one works image.Source = new FileImageSourceStub(firstPath); handler.UpdateValue(nameof(IImage.Source)); await image.WaitUntilLoaded(); - await platformView.AssertContainsColor(Colors.Blue.ToPlatform(), MauiContext); + await platformView.AssertContainsColor(Colors.Blue, MauiContext); // the second one does not image.Source = new FileImageSourceStub(secondPath); handler.UpdateValue(nameof(IImage.Source)); await image.WaitUntilLoaded(); - await platformView.AssertContainsColor(expectedColor.ToPlatform(), MauiContext); + await platformView.AssertContainsColor(expectedColor, MauiContext); }); }); } [Theory( -#if _ANDROID__ - Skip = "Test failing on ANDROID" +#if ANDROID || WINDOWS + Skip = "Test failing on ANDROID and WINDOWS" #endif )] [InlineData("red.png", "#FF0000")] @@ -116,7 +121,9 @@ await InvokeOnMainThreadAsync(async () => #endif )] [InlineData("animated_heart.gif", true)] +#if !WINDOWS [InlineData("animated_heart.gif", false)] +#endif public async virtual Task AnimatedSourceInitializesCorrectly(string filename, bool isAnimating) { var image = new TStub @@ -131,7 +138,7 @@ await InvokeOnMainThreadAsync(async () => await image.WaitUntilLoaded(); - await GetPlatformImageView(handler).AttachAndRun(() => + await AttachAndRun(GetPlatformImageView(handler), () => { Assert.Equal(isAnimating, GetNativeIsAnimationPlaying(handler)); }); @@ -196,7 +203,11 @@ await InvokeOnMainThreadAsync(async () => Assert.NotNull(exception); } - [Fact] + [Fact( +#if WINDOWS + Skip = "Hanging on Windows." +#endif + )] public async Task ImageLoadSequenceIsCorrect() { await ImageLoadSequenceIsCorrectImplementation(); @@ -259,12 +270,15 @@ public async Task ImageLoadSequenceIsCorrect() }); } - [Fact] + [Fact( +#if WINDOWS + Skip = "Hanging on Windows." +#endif + )] public async Task InterruptingLoadCancelsAndStartsOver() { await InterruptingLoadCancelsAndStartsOverImplementation(); } - async Task> InterruptingLoadCancelsAndStartsOverImplementation() { var image = new TStub @@ -325,7 +339,11 @@ await InvokeOnMainThreadAsync(async () => return events; } - [Theory] + [Theory( +#if WINDOWS + Skip = "To be implemented on Windows." +#endif + )] [InlineData("#FF0000")] [InlineData("#00FF00")] [InlineData("#000000")] @@ -353,7 +371,11 @@ await InvokeOnMainThreadAsync(async () => }); } - [Fact] + [Fact( +#if WINDOWS + Skip = "To be implemented on Windows." +#endif + )] public async Task InitializingSourceOnlyUpdatesImageOnce() { var image = new TStub @@ -382,7 +404,11 @@ await InvokeOnMainThreadAsync(async () => }); } - [Fact] + [Fact( +#if WINDOWS + Skip = "To be implemented on Windows." +#endif + )] public async Task UpdatingSourceOnlyUpdatesImageOnce() { var image = new TStub @@ -420,7 +446,11 @@ await InvokeOnMainThreadAsync(async () => }); } - [Fact] + [Fact( +#if WINDOWS + Skip = "Hanging on Windows." +#endif + )] public async Task ImageLoadSequenceIsCorrectWithChecks() { var events = await ImageLoadSequenceIsCorrectImplementation(); @@ -437,7 +467,11 @@ public async Task ImageLoadSequenceIsCorrectWithChecks() #endif } - [Fact] + [Fact( +#if WINDOWS + Skip = "Hanging on Windows." +#endif + )] public async Task InterruptingLoadCancelsAndStartsOverWithChecks() { var events = await InterruptingLoadCancelsAndStartsOverImplementation(); @@ -475,7 +509,11 @@ static int GetDrawableId(string image) => MauiProgram.DefaultContext.Resources.GetDrawableId(MauiProgram.DefaultContext.PackageName, image); #endif - [Fact] + [Fact( +#if WINDOWS + Skip = "To be implemented on Windows." +#endif + )] public async Task UpdatingSourceToNullClearsImage() { var image = new TStub @@ -503,7 +541,11 @@ await InvokeOnMainThreadAsync(async () => }); } - [Fact] + [Fact( +#if WINDOWS + Skip = "To be implemented on Windows." +#endif + )] public async Task UpdatingSourceToNonexistentSourceClearsImage() { var image = new TStub @@ -520,7 +562,7 @@ await InvokeOnMainThreadAsync(async () => image.Source = new FileImageSourceStub("fail.png"); handler.UpdateValue(nameof(IImage.Source)); - await handler.PlatformView.AttachAndRun(() => { }); + await AttachAndRun(handler.PlatformView, () => { }); await image.WaitUntilLoaded(5000); await handler.PlatformView.AssertDoesNotContainColor(Colors.Red, MauiContext); diff --git a/src/Core/tests/DeviceTests/Services/ImageSource/BaseImageSourceServiceTests.Windows.cs b/src/Core/tests/DeviceTests/Services/ImageSource/BaseImageSourceServiceTests.Windows.cs new file mode 100644 index 000000000000..792aee1a4dae --- /dev/null +++ b/src/Core/tests/DeviceTests/Services/ImageSource/BaseImageSourceServiceTests.Windows.cs @@ -0,0 +1,94 @@ +using System; +using System.IO; +using System.Runtime.InteropServices.WindowsRuntime; +using System.Text; +using Microsoft.Maui.Devices; +using Microsoft.Maui.Graphics; +using Microsoft.Maui.Storage; +using Microsoft.UI.Xaml.Media.Imaging; +using Windows.Graphics.Imaging; +using Xunit; +using WColor = Windows.UI.Color; + +namespace Microsoft.Maui.DeviceTests +{ + public abstract partial class BaseImageSourceServiceTests + { + public static string CreateBitmapFile(int width, int height, Color color, string filename = null) => + CreateBitmapFile(width, height, color.ToWindowsColor(), filename); + + public static string CreateBitmapFile(int width, int height, WColor color, string filename = null) + { + filename ??= Guid.NewGuid().ToString("N") + ".png"; + if (!Path.IsPathRooted(filename)) + { + filename = Path.Combine(FileSystem.CacheDirectory, Guid.NewGuid().ToString("N"), filename); + } + var dir = Path.GetDirectoryName(filename); + Directory.CreateDirectory(dir); + + using var src = CreateBitmapStream(width, height, color); + using var dst = File.Create(filename); + src.CopyTo(dst); + + return filename; + } + + public static Stream CreateBitmapStream(int width, int height, Color color) => + CreateBitmapStream(width, height, color.ToWindowsColor()); + + public static Stream CreateBitmapStream(int width, int height, WColor color) + { + var bitmap = CreateBitmap(width, height, color); + + var stream = new MemoryStream(); + + var encoder = BitmapEncoder.CreateAsync(BitmapEncoder.PngEncoderId, stream.AsRandomAccessStream()).GetAwaiter().GetResult(); + + encoder.SetPixelData( + BitmapPixelFormat.Bgra8, + BitmapAlphaMode.Ignore, + (uint)bitmap.PixelWidth, + (uint)bitmap.PixelHeight, + 96, + 96, + bitmap.PixelBuffer.ToArray()); + + stream.Position = 0; + + return stream; + } + + public static WriteableBitmap CreateBitmap(int width, int height, Color color) => + CreateBitmap(width, height, color.ToWindowsColor()); + + public static WriteableBitmap CreateBitmap(int width, int height, WColor color) + { + var bitmap = new WriteableBitmap(width, height); + + using (var stream = bitmap.PixelBuffer.AsStream()) + { + var pixels = new byte[width * height * 4]; + + for (var y = 0; y < height; y++) + { + for (var x = 0; x < width; x++) + { + var index = (y * width + x) * 4; + + pixels[index + 0] = color.B; + pixels[index + 1] = color.G; + pixels[index + 2] = color.R; + pixels[index + 3] = color.A; + } + } + + stream.Write(pixels, 0, pixels.Length); + } + + bitmap.Invalidate(); + + return bitmap; + } + } +} \ No newline at end of file diff --git a/src/Core/tests/DeviceTests/Stubs/CountedImageHandler.Windows.cs b/src/Core/tests/DeviceTests/Stubs/CountedImageHandler.Windows.cs new file mode 100644 index 000000000000..df80cc2ab31f --- /dev/null +++ b/src/Core/tests/DeviceTests/Stubs/CountedImageHandler.Windows.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using WImage = Microsoft.UI.Xaml.Controls.Image; + +namespace Microsoft.Maui.DeviceTests.Stubs +{ + public partial class CountedImageHandler + { + protected override WImage CreatePlatformView() + { + var image = new WImage(); + image.RegisterPropertyChangedCallback(WImage.SourceProperty, (s, e) => Log(image.Source, "Source")); + return image; + } + + public List<(string Member, object Value)> ImageEvents { get; } = new List<(string, object)>(); + + void Log(object value, string member) + { + ImageEvents.Add((member, value)); + } + } +} diff --git a/src/TestUtils/src/DeviceTests/AssertionExtensions.Windows.cs b/src/TestUtils/src/DeviceTests/AssertionExtensions.Windows.cs index d6cc205fd04d..045ca092eda5 100644 --- a/src/TestUtils/src/DeviceTests/AssertionExtensions.Windows.cs +++ b/src/TestUtils/src/DeviceTests/AssertionExtensions.Windows.cs @@ -147,6 +147,13 @@ public static Task AttachAndRun(this FrameworkElement view, Action actio public static Task AttachAndRun(this FrameworkElement view, Func action, IMauiContext mauiContext) => view.AttachAndRun(window => action(), mauiContext); + public static Task AttachAndRun(this FrameworkElement view, Func action, IMauiContext mauiContext) => + view.AttachAndRun(async window => + { + await action(); + return true; + }, mauiContext); + public static Task AttachAndRun(this FrameworkElement view, Func action, IMauiContext mauiContext) => view.AttachAndRun((window) => {