Skip to content
Merged
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
103 changes: 99 additions & 4 deletions src/Controls/tests/TestCases.Shared.Tests/UITest.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System.Reflection;
using System.Text.RegularExpressions;
using ImageMagick;
using NUnit.Framework;
using NUnit.Framework.Constraints;
using UITest.Appium;
using UITest.Appium.NUnit;
using UITest.Core;
Expand Down Expand Up @@ -123,15 +125,16 @@ public void VerifyScreenshotOrSetException(
string? name = null,
TimeSpan? retryDelay = null,
int cropTop = 0,
int cropBottom = 0
int cropBottom = 0,
double tolerance = 0.0
#if MACUITEST || WINTEST
, bool includeTitleBar = false
#endif
)
{
try
{
VerifyScreenshot(name, retryDelay, cropTop, cropBottom
VerifyScreenshot(name, retryDelay, cropTop, cropBottom, tolerance
#if MACUITEST || WINTEST
, includeTitleBar
#endif
Expand All @@ -143,11 +146,42 @@ public void VerifyScreenshotOrSetException(
}
}

/// <summary>
/// Verifies a screenshot by comparing it against a baseline image and throws an exception if verification fails.
/// </summary>
/// <param name="name">Optional name for the screenshot. If not provided, a default name will be used.</param>
/// <param name="retryDelay">Optional delay between retry attempts when verification fails.</param>
/// <param name="cropTop">Number of pixels to crop from the top of the screenshot.</param>
/// <param name="cropBottom">Number of pixels to crop from the bottom of the screenshot.</param>
/// <param name="tolerance">Tolerance level for image comparison as a percentage from 0 to 100.</param>
#if MACUITEST || WINTEST
/// <param name="includeTitleBar">Whether to include the title bar in the screenshot comparison.</param>
#endif
/// <remarks>
/// This method immediately throws an exception if the screenshot verification fails.
/// For batch verification of multiple screenshots, consider using <see cref="VerifyScreenshotOrSetException"/> instead.
/// </remarks>
/// <example>
/// <code>
/// // Exact match (no tolerance)
/// VerifyScreenshot("LoginScreen");
///
/// // Allow 2% difference for dynamic content
/// VerifyScreenshot("DashboardWithTimestamp", tolerance: 2.0);
///
/// // Allow 5% difference for animations or slight rendering variations
/// VerifyScreenshot("ButtonHoverState", tolerance: 5.0);
///
/// // Combined with cropping and tolerance
/// VerifyScreenshot("HeaderSection", cropTop: 50, cropBottom: 100, tolerance: 3.0);
/// </code>
/// </example>
public void VerifyScreenshot(
string? name = null,
TimeSpan? retryDelay = null,
int cropTop = 0,
int cropBottom = 0
int cropBottom = 0,
double tolerance = 0.0 // Add tolerance parameter (0.05 = 5%)
#if MACUITEST || WINTEST
, bool includeTitleBar = false
#endif
Expand Down Expand Up @@ -291,8 +325,69 @@ but both can happen.
actualImage = imageEditor.GetUpdatedImage();
}

_visualRegressionTester.VerifyMatchesSnapshot(name!, actualImage, environmentName: environmentName, testContext: _visualTestContext);
// Apply tolerance if specified
if (tolerance > 0)
{
VerifyWithTolerance(name!, actualImage, environmentName, tolerance);
}
else
{
_visualRegressionTester.VerifyMatchesSnapshot(name!, actualImage, environmentName: environmentName, testContext: _visualTestContext);
}
}
}

void VerifyWithTolerance(string name, ImageSnapshot actualImage, string environmentName, double tolerance)
{
if (tolerance > 15)
{
throw new ArgumentException($"Tolerance {tolerance}% exceeds the acceptable limit. Please review whether this requires a different test or if it is a bug.");
}

try
{
_visualRegressionTester.VerifyMatchesSnapshot(name, actualImage, environmentName: environmentName, testContext: _visualTestContext);
}
catch (Exception ex) when (IsVisualDifferenceException(ex))
{
var difference = ExtractDifferencePercentage(ex);
if (difference <= tolerance)
{
// Log warning but pass test
TestContext.WriteLine($"Visual difference {difference}% within tolerance {tolerance}% for '{name}' on {environmentName}");
return;
}
throw; // Re-throw if exceeds tolerance
}
}

bool IsVisualDifferenceException(Exception ex)
{
// Check if this is a visual regression failure
return ex.GetType().Name.Contains("Assert", StringComparison.Ordinal) ||
ex.Message.Contains("Snapshot different", StringComparison.Ordinal) ||
ex.Message.Contains("baseline", StringComparison.Ordinal) ||
ex.Message.Contains("different", StringComparison.Ordinal);
}

double ExtractDifferencePercentage(Exception ex)
{
var message = ex.Message;

// Extract percentage from pattern: "X,XX% difference"
var match = Regex.Match(message, @"(\d+,\d+)%\s*difference", RegexOptions.IgnoreCase);
if (match.Success)
{
var percentageString = match.Groups[1].Value.Replace(',', '.');
if (double.TryParse(percentageString, System.Globalization.NumberStyles.Float,
System.Globalization.CultureInfo.InvariantCulture, out var percentage))
{
return percentage;
}
}

// If can't extract specific percentage, throw an exception to indicate failure
throw new InvalidOperationException("Unable to extract difference percentage from exception message.");
}

protected void VerifyInternetConnectivity()
Expand Down
Loading