From d12ef225f2b7a81c831c4f721ae3f5bfe5065d2a Mon Sep 17 00:00:00 2001 From: Matthew Leibowitz Date: Tue, 6 Feb 2024 22:27:43 +0200 Subject: [PATCH] Fix Windows device tests - Some views need a few ms to finish animating The switch on WinUI needs to animate to "on" so we need to wait a few ms so that the control is ready for the screenshots/color comparisons. - We now have too many tests for WinUI to handle We need to break up all the tests by category to avoid "running out of windows" --- eng/devices/windows.cake | 124 +++++++----------- .../DeviceTests.Shared/MauiProgramDefaults.cs | 19 +-- .../Handlers/Element/ElementTests.cs | 1 + .../SwipeView/SwipeViewHandlerTests.cs | 20 +++ .../Switch/SwitchHandlerTests.Windows.cs | 4 +- .../Handlers/Switch/SwitchHandlerTests.cs | 19 +-- .../tests/DeviceTests/Memory/MemoryTests.cs | 1 + src/Core/tests/DeviceTests/TestCategory.cs | 1 + .../AppHostBuilderExtensions.cs | 17 +-- ...er.cs => PerCategoryHeadlessTestRunner.cs} | 35 ++++- .../VisualRunner/Pages/HomePage.xaml.cs | 15 +-- .../AssertionExtensions.Windows.cs | 12 +- 12 files changed, 130 insertions(+), 138 deletions(-) rename src/TestUtils/src/DeviceTests.Runners/HeadlessRunner/Windows/{ControlsHeadlessTestRunner.cs => PerCategoryHeadlessTestRunner.cs} (80%) diff --git a/eng/devices/windows.cake b/eng/devices/windows.cake index d9cee85ea9ff..d7bd5faae459 100644 --- a/eng/devices/windows.cake +++ b/eng/devices/windows.cake @@ -36,7 +36,6 @@ string DOTNET_PLATFORM = $"win10-x64"; bool DEVICE_CLEANUP = Argument("cleanup", true); string certificateThumbprint = ""; bool isPackagedTestRun = TEST_DEVICE.ToLower().Equals("packaged"); -bool isControlsProjectTestRun = PROJECT.FullPath.EndsWith("Controls.DeviceTests.csproj"); // Certificate Common Name to use/generate (eg: CN=DotNetMauiTests) var certCN = Argument("commonname", "DotNetMAUITests"); @@ -250,38 +249,26 @@ Task("Test") // Install the DeviceTests app StartProcess("powershell", "Add-AppxPackage -Path \"" + MakeAbsolute(msixPath).FullPath + "\""); - if (isControlsProjectTestRun) - { - // Start the app once, this will trigger the discovery of the test categories - var startArgsInitial = "Start-Process shell:AppsFolder\\$((Get-AppxPackage -Name \"" + PACKAGEID + "\").PackageFamilyName)!App -ArgumentList \"" + testResultsFile + "\", \"-1\""; - StartProcess("powershell", startArgsInitial); - - Information($"Waiting 10 seconds for process to finish..."); - System.Threading.Thread.Sleep(10000); - - var testCategoriesToRun = System.IO.File.ReadAllLines(testsToRunFile).Length; + // Start the app once, this will trigger the discovery of the test categories + var startArgsInitial = "Start-Process shell:AppsFolder\\$((Get-AppxPackage -Name \"" + PACKAGEID + "\").PackageFamilyName)!App -ArgumentList \"" + testResultsFile + "\", \"-1\""; + StartProcess("powershell", startArgsInitial); - for (int i = 0; i <= testCategoriesToRun; i++) - { - var startArgs = "Start-Process shell:AppsFolder\\$((Get-AppxPackage -Name \"" + PACKAGEID + "\").PackageFamilyName)!App -ArgumentList \"" + testResultsFile + "\", \"" + i + "\""; - - Information(startArgs); + Information($"Waiting 10 seconds for process to finish..."); + System.Threading.Thread.Sleep(10000); - // Start the DeviceTests app for packaged - StartProcess("powershell", startArgs); + var testCategoriesToRun = System.IO.File.ReadAllLines(testsToRunFile).Length; - Information($"Waiting 10 seconds for the next..."); - System.Threading.Thread.Sleep(10000); - } - } - else + for (int i = 0; i <= testCategoriesToRun; i++) { - var startArgs = "Start-Process shell:AppsFolder\\$((Get-AppxPackage -Name \"" + PACKAGEID + "\").PackageFamilyName)!App -ArgumentList \"" + testResultsFile + "\""; + var startArgs = "Start-Process shell:AppsFolder\\$((Get-AppxPackage -Name \"" + PACKAGEID + "\").PackageFamilyName)!App -ArgumentList \"" + testResultsFile + "\", \"" + i + "\""; Information(startArgs); // Start the DeviceTests app for packaged StartProcess("powershell", startArgs); + + Information($"Waiting 10 seconds for the next..."); + System.Threading.Thread.Sleep(10000); } } else @@ -289,22 +276,15 @@ Task("Test") // Unpackaged process blocks the thread, so we can wait shorter for the results waitForResultTimeoutInSeconds = 30; - if (isControlsProjectTestRun) - { - // Start the app once, this will trigger the discovery of the test categories - StartProcess(TEST_APP, testResultsFile + " -1"); + // Start the app once, this will trigger the discovery of the test categories + StartProcess(TEST_APP, testResultsFile + " -1"); - var testCategoriesToRun = System.IO.File.ReadAllLines(testsToRunFile).Length; + var testCategoriesToRun = System.IO.File.ReadAllLines(testsToRunFile).Length; - for (int i = 0; i <= testCategoriesToRun; i++) - { - // Start the DeviceTests app for unpackaged - StartProcess(TEST_APP, testResultsFile + " " + i); - } - } - else + for (int i = 0; i <= testCategoriesToRun; i++) { - StartProcess(TEST_APP, testResultsFile); + // Start the DeviceTests app for unpackaged + StartProcess(TEST_APP, testResultsFile + " " + i); } } @@ -318,49 +298,45 @@ Task("Test") break; } - // If we're running the Controls project, double-check if we have all test result files - // and if the categories we expected to run match the test result files - if (isControlsProjectTestRun) + // Double-check if we have all test result files and if the categories we expected to run match the test result files + var expectedCategories = System.IO.File.ReadAllLines(testsToRunFile); + var expectedCategoriesRanCount = expectedCategories.Length; + var actualResultFileCount = System.IO.Directory.GetFiles(testResultsPath, "TestResults-*.xml").Length; + + while (actualResultFileCount < expectedCategoriesRanCount) { + actualResultFileCount = System.IO.Directory.GetFiles(testResultsPath, "TestResults-*.xml").Length; + System.Threading.Thread.Sleep(1000); + waited++; + + Information($"Waiting {waited} additional second(s) for tests to finish..."); + if (waited >= 30) + break; + } + + if (FileExists(testsToRunFile)) { - var expectedCategories = System.IO.File.ReadAllLines(testsToRunFile); - var expectedCategoriesRanCount = expectedCategories.Length; - var actualResultFileCount = System.IO.Directory.GetFiles(testResultsPath, "TestResults-*.xml").Length; - - while (actualResultFileCount < expectedCategoriesRanCount) { - actualResultFileCount = System.IO.Directory.GetFiles(testResultsPath, "TestResults-*.xml").Length; - System.Threading.Thread.Sleep(1000); - waited++; - - Information($"Waiting {waited} additional second(s) for tests to finish..."); - if (waited >= 30) - break; - } - - if (FileExists(testsToRunFile)) - { - DeleteFile(testsToRunFile); - } + DeleteFile(testsToRunFile); + } - // While the count should match exactly, if we get more files somehow we'll allow it - // If it's less, throw an exception to fail the pipeline. - if (actualResultFileCount < expectedCategoriesRanCount) - { - // Grab the category name from the file name - // Ex: "TestResults-com_microsoft_maui_controls_devicetests_Frame.xml" -> "Frame" - var actualFiles = System.IO.Directory.GetFiles(testResultsPath, "TestResults-*.xml"); - var actualCategories = actualFiles.Select(x => x.Substring(0, x.Length - 4) // Remove ".xml" - .Split('_').Last()).ToList(); + // While the count should match exactly, if we get more files somehow we'll allow it + // If it's less, throw an exception to fail the pipeline. + if (actualResultFileCount < expectedCategoriesRanCount) + { + // Grab the category name from the file name + // Ex: "TestResults-com_microsoft_maui_controls_devicetests_Frame.xml" -> "Frame" + var actualFiles = System.IO.Directory.GetFiles(testResultsPath, "TestResults-*.xml"); + var actualCategories = actualFiles.Select(x => x.Substring(0, x.Length - 4) // Remove ".xml" + .Split('_').Last()).ToList(); - foreach (var category in expectedCategories) + foreach (var category in expectedCategories) + { + if (!actualCategories.Contains(category)) { - if (!actualCategories.Contains(category)) - { - Error($"Error: missing test file result for {category}"); - } + Error($"Error: missing test file result for {category}"); } - - throw new Exception($"Expected test result files: {expectedCategoriesRanCount}, actual files: {actualResultFileCount}, some process(es) might have crashed."); } + + throw new Exception($"Expected test result files: {expectedCategoriesRanCount}, actual files: {actualResultFileCount}, some process(es) might have crashed."); } if(System.IO.Directory.GetFiles(testResultsPath, "TestResults-*.xml").Length == 0) diff --git a/src/Core/tests/DeviceTests.Shared/MauiProgramDefaults.cs b/src/Core/tests/DeviceTests.Shared/MauiProgramDefaults.cs index a99ca72d0133..b11e979c831d 100644 --- a/src/Core/tests/DeviceTests.Shared/MauiProgramDefaults.cs +++ b/src/Core/tests/DeviceTests.Shared/MauiProgramDefaults.cs @@ -45,28 +45,11 @@ public static MauiApp CreateMauiApp(List testAssemblies) Assemblies = testAssemblies, }); -#if WINDOWS - if (testAssemblies.Any(a => a.FullName.Contains("Controls.DeviceTests", - StringComparison.OrdinalIgnoreCase))) - { - appBuilder.UseControlsHeadlessRunner(new HeadlessRunnerOptions - { - RequiresUIContext = true, - }); - } - else - { - appBuilder.UseHeadlessRunner(new HeadlessRunnerOptions - { - RequiresUIContext = true, - }); - } -#else appBuilder.UseHeadlessRunner(new HeadlessRunnerOptions { RequiresUIContext = true, }); -#endif + appBuilder.UseVisualRunner(); appBuilder.ConfigureContainer(new DefaultServiceProviderFactory(new ServiceProviderOptions diff --git a/src/Core/tests/DeviceTests/Handlers/Element/ElementTests.cs b/src/Core/tests/DeviceTests/Handlers/Element/ElementTests.cs index 4aa55ca9a8cf..d9c71abef49e 100644 --- a/src/Core/tests/DeviceTests/Handlers/Element/ElementTests.cs +++ b/src/Core/tests/DeviceTests/Handlers/Element/ElementTests.cs @@ -8,6 +8,7 @@ namespace Microsoft.Maui.DeviceTests { + [Category(TestCategory.Element)] public partial class ElementTests : CoreHandlerTestBase { [Fact] diff --git a/src/Core/tests/DeviceTests/Handlers/SwipeView/SwipeViewHandlerTests.cs b/src/Core/tests/DeviceTests/Handlers/SwipeView/SwipeViewHandlerTests.cs index fb163198f04f..21da42ff712a 100644 --- a/src/Core/tests/DeviceTests/Handlers/SwipeView/SwipeViewHandlerTests.cs +++ b/src/Core/tests/DeviceTests/Handlers/SwipeView/SwipeViewHandlerTests.cs @@ -6,5 +6,25 @@ namespace Microsoft.Maui.DeviceTests [Category(TestCategory.SwipeView)] public partial class SwipeViewHandlerTests : CoreHandlerTestBase { + [Fact(DisplayName = "Background Initializes Correctly")] + public async Task BackgroundInitializesCorrectly() + { + var brush = new SolidPaintStub(Colors.Blue); + + var label = new SwipeViewStub() + { + Background = brush, + Content = new LabelStub { Text = "Swipe Me" }, + LeftItems = + { + new SwipeItemStub + { + Background = new SolidPaintStub(Colors.Red) + } + } + }; + + await ValidateHasColor(label, Colors.Blue); + } } } \ No newline at end of file diff --git a/src/Core/tests/DeviceTests/Handlers/Switch/SwitchHandlerTests.Windows.cs b/src/Core/tests/DeviceTests/Handlers/Switch/SwitchHandlerTests.Windows.cs index 98cc0b4c267b..7738c29d08b1 100644 --- a/src/Core/tests/DeviceTests/Handlers/Switch/SwitchHandlerTests.Windows.cs +++ b/src/Core/tests/DeviceTests/Handlers/Switch/SwitchHandlerTests.Windows.cs @@ -17,9 +17,9 @@ bool GetNativeIsOn(SwitchHandler switchHandler) => GetNativeSwitch(switchHandler).IsOn; Task ValidateTrackColor(ISwitch switchStub, Color color, Action action = null, string updatePropertyValue = null) => - ValidateHasColor(switchStub, color, action, updatePropertyValue: updatePropertyValue); + ValidateHasColor(switchStub, color, action, updatePropertyValue: updatePropertyValue, .01); Task ValidateThumbColor(ISwitch switchStub, Color color, Action action = null, string updatePropertyValue = null) => - ValidateHasColor(switchStub, color, action, updatePropertyValue: updatePropertyValue); + ValidateHasColor(switchStub, color, action, updatePropertyValue: updatePropertyValue, .01); } } \ No newline at end of file diff --git a/src/Core/tests/DeviceTests/Handlers/Switch/SwitchHandlerTests.cs b/src/Core/tests/DeviceTests/Handlers/Switch/SwitchHandlerTests.cs index c50ef245bdb1..b64a252403c6 100644 --- a/src/Core/tests/DeviceTests/Handlers/Switch/SwitchHandlerTests.cs +++ b/src/Core/tests/DeviceTests/Handlers/Switch/SwitchHandlerTests.cs @@ -48,10 +48,7 @@ public async Task TrackColorInitializesCorrectly(bool isToggled) TrackColor = Colors.Red }; - await AttachAndRun(switchStub, async (handler) => - { - await ValidateTrackColor(switchStub, Colors.Red); - }); + await ValidateTrackColor(switchStub, Colors.Red); } [Fact(DisplayName = "Track Color Updates Correctly")] @@ -61,13 +58,11 @@ public async Task TrackColorUpdatesCorrectly() { IsOn = true }; - await AttachAndRun(switchStub, async (handler) => - { - await ValidateTrackColor(switchStub, Colors.Red, () => switchStub.TrackColor = Colors.Red); - }); + + await ValidateTrackColor(switchStub, Colors.Red, () => switchStub.TrackColor = Colors.Red); } - [Fact(DisplayName = "ThumbColor Initializes Correctly", Skip = "There seems to be an issue, so disable for now: https://github.com/dotnet/maui/issues/1275")] + [Fact(DisplayName = "ThumbColor Initializes Correctly")] public async Task ThumbColorInitializesCorrectly() { var switchStub = new SwitchStub() @@ -92,11 +87,7 @@ public async Task NullThumbColorDoesntCrash() await CreateHandlerAsync(switchStub); } - [Fact(DisplayName = "Thumb Color Updates Correctly" -#if WINDOWS - , Skip = "Failing on Windows" -#endif - )] + [Fact(DisplayName = "Thumb Color Updates Correctly")] public async Task ThumbColorUpdatesCorrectly() { var switchStub = new SwitchStub() diff --git a/src/Core/tests/DeviceTests/Memory/MemoryTests.cs b/src/Core/tests/DeviceTests/Memory/MemoryTests.cs index fd31b1e83f2d..11ad93dea2cc 100644 --- a/src/Core/tests/DeviceTests/Memory/MemoryTests.cs +++ b/src/Core/tests/DeviceTests/Memory/MemoryTests.cs @@ -14,6 +14,7 @@ namespace Microsoft.Maui.Handlers.Memory /// which seems to make Android and WinUI happier. Low APIs on Android still have issues running via xHarness /// which is why we only currently run these on API 30+ /// + [Category(TestCategory.Memory)] [TestCaseOrderer("Microsoft.Maui.Handlers.Memory.MemoryTestOrdering", "Microsoft.Maui.Core.DeviceTests")] public class MemoryTests : CoreHandlerTestBase, IClassFixture { diff --git a/src/Core/tests/DeviceTests/TestCategory.cs b/src/Core/tests/DeviceTests/TestCategory.cs index 6e998ab33aba..530ccdc8a36b 100644 --- a/src/Core/tests/DeviceTests/TestCategory.cs +++ b/src/Core/tests/DeviceTests/TestCategory.cs @@ -42,6 +42,7 @@ public static class TestCategory public const string TextFormatting = "Formatting"; public const string TimePicker = "TimePicker"; public const string View = "View"; + public const string Element = "Element"; public const string WebView = "WebView"; public const string Window = "Window"; public const string WindowOverlay = "WindowOverlay"; diff --git a/src/TestUtils/src/DeviceTests.Runners/AppHostBuilderExtensions.cs b/src/TestUtils/src/DeviceTests.Runners/AppHostBuilderExtensions.cs index b66e18cb1c8b..e03d87c0db33 100644 --- a/src/TestUtils/src/DeviceTests.Runners/AppHostBuilderExtensions.cs +++ b/src/TestUtils/src/DeviceTests.Runners/AppHostBuilderExtensions.cs @@ -30,26 +30,17 @@ public static MauiAppBuilder UseHeadlessRunner(this MauiAppBuilder appHostBuilde { appHostBuilder.Services.AddSingleton(options); -#if __ANDROID__ || __IOS__ || MACCATALYST || WINDOWS +#if __ANDROID__ || __IOS__ || MACCATALYST appHostBuilder.Services.AddTransient(svc => new HeadlessTestRunner( svc.GetRequiredService(), svc.GetRequiredService())); -#endif - - return appHostBuilder; - } - -#if WINDOWS - public static MauiAppBuilder UseControlsHeadlessRunner(this MauiAppBuilder appHostBuilder, HeadlessRunnerOptions options) - { - appHostBuilder.Services.AddSingleton(options); - - appHostBuilder.Services.AddTransient(svc => new ControlsHeadlessTestRunner( +#elif WINDOWS + appHostBuilder.Services.AddTransient(svc => new PerCategoryHeadlessTestRunner( svc.GetRequiredService(), svc.GetRequiredService())); +#endif return appHostBuilder; } -#endif } } \ No newline at end of file diff --git a/src/TestUtils/src/DeviceTests.Runners/HeadlessRunner/Windows/ControlsHeadlessTestRunner.cs b/src/TestUtils/src/DeviceTests.Runners/HeadlessRunner/Windows/PerCategoryHeadlessTestRunner.cs similarity index 80% rename from src/TestUtils/src/DeviceTests.Runners/HeadlessRunner/Windows/ControlsHeadlessTestRunner.cs rename to src/TestUtils/src/DeviceTests.Runners/HeadlessRunner/Windows/PerCategoryHeadlessTestRunner.cs index 623099ad9532..935d93d6c69b 100644 --- a/src/TestUtils/src/DeviceTests.Runners/HeadlessRunner/Windows/ControlsHeadlessTestRunner.cs +++ b/src/TestUtils/src/DeviceTests.Runners/HeadlessRunner/Windows/PerCategoryHeadlessTestRunner.cs @@ -8,10 +8,11 @@ using Microsoft.DotNet.XHarness.TestRunners.Common; using Microsoft.DotNet.XHarness.TestRunners.Xunit; using Xunit; +using Xunit.Abstractions; namespace Microsoft.Maui.TestUtils.DeviceTests.Runners.HeadlessRunner { - public class ControlsHeadlessTestRunner : AndroidApplicationEntryPoint + public class PerCategoryHeadlessTestRunner : AndroidApplicationEntryPoint { const string CategoriesFileName = "devicetestcategories.txt"; readonly string _categoriesFilePath; @@ -25,7 +26,7 @@ public class ControlsHeadlessTestRunner : AndroidApplicationEntryPoint readonly int _loopCount; TestLogger _logger; - public ControlsHeadlessTestRunner(HeadlessRunnerOptions runnerOptions, TestOptions options) + public PerCategoryHeadlessTestRunner(HeadlessRunnerOptions runnerOptions, TestOptions options) { _runnerOptions = runnerOptions; _options = options; @@ -41,7 +42,7 @@ public ControlsHeadlessTestRunner(HeadlessRunnerOptions runnerOptions, TestOptio public override string TestsResultsFinalPath => _resultsPath!; - protected override int? MaxParallelThreads => System.Environment.ProcessorCount; + protected override int? MaxParallelThreads => Environment.ProcessorCount; protected override IDevice Device { get; } = new TestDevice(); @@ -88,7 +89,7 @@ protected override TestRunner GetTestRunner(LogWriter logWriter) if (_loopCount == -1) { var categories = DiscoverTestsInAssemblies(); - File.WriteAllLines(_categoriesFilePath, categories.ToArray()); + File.WriteAllLines(_categoriesFilePath, categories); TerminateWithSuccess(); return null; @@ -121,9 +122,9 @@ void OnTestsCompleted(object? sender, TestRunResult results) } } - IEnumerable DiscoverTestsInAssemblies() + ICollection DiscoverTestsInAssemblies() { - var result = new List(); + var result = new HashSet(); try { @@ -142,7 +143,27 @@ IEnumerable DiscoverTestsInAssemblies() framework.Find(false, sink, discoveryOptions); sink.Finished.WaitOne(); - result.AddRange(sink.TestCases.SelectMany(tc => tc.Traits["Category"]).Distinct()); + var skipped = new HashSet(); + + foreach (var test in sink.TestCases) + { + if (test.Traits.TryGetValue("Category", out var categories)) + { + foreach (var category in categories) + { + result.Add(category); + } + } + else + { + skipped.Add($"{test.TestMethod.TestClass.Class.Name}"); + } + } + + if (skipped.Count > 0) + { + throw new Exception($"Some tests do not have a category: {string.Join(", ", skipped)}"); + } } } catch (Exception e) diff --git a/src/TestUtils/src/DeviceTests.Runners/VisualRunner/Pages/HomePage.xaml.cs b/src/TestUtils/src/DeviceTests.Runners/VisualRunner/Pages/HomePage.xaml.cs index 7f7707af494d..b29c8288d4db 100644 --- a/src/TestUtils/src/DeviceTests.Runners/VisualRunner/Pages/HomePage.xaml.cs +++ b/src/TestUtils/src/DeviceTests.Runners/VisualRunner/Pages/HomePage.xaml.cs @@ -25,10 +25,11 @@ private async void HomePage_Loaded(object? sender, EventArgs e) #if WINDOWS var cliArgs = Environment.GetCommandLineArgs(); + cliArgs = cliArgs.Union(new[] { "C:\\Projects\\maui\\artifacts\\testing\\TestResults-com_microsoft_maui_core_devicetests.xml", "-1" }).ToArray(); if (cliArgs.Length > 1) { - testResultsFile = HeadlessTestRunner.TestResultsFile = ControlsHeadlessTestRunner.TestResultsFile = cliArgs.Skip(1).FirstOrDefault(); - ControlsHeadlessTestRunner.LoopCount = int.Parse(cliArgs.Skip(2).FirstOrDefault() ?? "-1"); + testResultsFile = HeadlessTestRunner.TestResultsFile = PerCategoryHeadlessTestRunner.TestResultsFile = cliArgs.Skip(1).FirstOrDefault(); + PerCategoryHeadlessTestRunner.LoopCount = int.Parse(cliArgs.Skip(2).FirstOrDefault() ?? "-1"); } #endif @@ -36,22 +37,18 @@ private async void HomePage_Loaded(object? sender, EventArgs e) { hasRunHeadless = true; -#if !WINDOWS - var headlessRunner = Handler!.MauiContext!.Services.GetRequiredService(); - - await headlessRunner.RunTestsAsync(); -#else +#if WINDOWS if (cliArgs.Length >= 3) { - var headlessRunner = Handler!.MauiContext!.Services.GetRequiredService(); + var headlessRunner = Handler!.MauiContext!.Services.GetRequiredService(); await headlessRunner.RunTestsAsync(); } else +#endif { var headlessRunner = Handler!.MauiContext!.Services.GetRequiredService(); await headlessRunner.RunTestsAsync(); } -#endif Process.GetCurrentProcess().Kill(); } diff --git a/src/TestUtils/src/DeviceTests/AssertionExtensions.Windows.cs b/src/TestUtils/src/DeviceTests/AssertionExtensions.Windows.cs index 045ca092eda5..b2f2adc86eeb 100644 --- a/src/TestUtils/src/DeviceTests/AssertionExtensions.Windows.cs +++ b/src/TestUtils/src/DeviceTests/AssertionExtensions.Windows.cs @@ -231,6 +231,12 @@ public static async Task AttachAndRun(this FrameworkElement view, Func action(window)); } finally @@ -343,7 +349,11 @@ public static async Task AssertContainsColor(this CanvasBitmap bit foreach (var c in colors) { - if (c.IsEquivalent(expectedColor)) + var precision = tolerance is null + ? ColorComparison.ColorPrecision + : (int)(tolerance * 255); + + if (c.IsEquivalent(expectedColor, precision)) { return bitmap; }