/*
* Copyright (c) 2026 Proton AG
*
* This file is part of ProtonVPN.
*
* ProtonVPN is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* ProtonVPN is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with ProtonVPN. If not, see .
*/
using System;
using System.Linq;
using System.Drawing;
using System.Threading;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using FlaUI.Core.Input;
using FlaUI.Core.Tools;
using FlaUI.Core.Patterns;
using FlaUI.Core.Conditions;
using FlaUI.Core.Definitions;
using FlaUI.Core.AutomationElements;
using NUnit.Framework;
using ProtonVPN.UI.Tests.TestBase;
using ProtonVPN.UI.Tests.TestsHelper;
namespace ProtonVPN.UI.Tests.UiTools;
public static class UiActions
{
public static T Click(this T desiredElement, TimeSpan? retryIntervalOverload = null) where T : Element
{
AutomationElement? elementToClick = WaitUntilExists(desiredElement, TestConstants.EighteenSecondsTimeout, retryIntervalOverload);
elementToClick?.WaitUntilClickable(TestConstants.EighteenSecondsTimeout);
elementToClick?.Click();
return desiredElement;
}
public static T ClickUntilElementDisappears(this T desiredElement, TimeSpan? retryIntervalOverload = null) where T : Element
{
AutomationElement? elementToClick = WaitUntilExists(desiredElement, TestConstants.EighteenSecondsTimeout, retryIntervalOverload);
elementToClick?.WaitUntilClickable(TestConstants.EighteenSecondsTimeout);
DateTime timeoutDate = DateTime.UtcNow + TestConstants.FiveSecondsTimeout;
while (FindFirstDescendantUsingChildren(desiredElement.Condition) != null && (DateTime.UtcNow < timeoutDate))
{
elementToClick?.Click();
Thread.Sleep(TestConstants.AnimationDelay);
}
return desiredElement;
}
public static T DoubleClick(this T desiredElement) where T : Element
{
AutomationElement? elementToClick = WaitUntilExists(desiredElement);
elementToClick?.WaitUntilClickable(TestConstants.TenSecondsTimeout);
elementToClick?.DoubleClick();
return desiredElement;
}
public static T RightClick(this T desiredElement) where T : Element
{
AutomationElement? elementToClick = WaitUntilExists(desiredElement);
elementToClick?.WaitUntilClickable(TestConstants.TenSecondsTimeout);
elementToClick?.RightClick();
return desiredElement;
}
public static T Toggle(this T desiredElement) where T : Element
{
AutomationElement? elementToClick = WaitUntilExists(desiredElement);
elementToClick?.WaitUntilClickable(TestConstants.TenSecondsTimeout);
elementToClick?.AsToggleButton().Toggle();
return desiredElement;
}
public static bool IsToggled(this T toggleElement, bool checkParent = false) where T : Element
{
AutomationElement? elementToCheck = WaitUntilExists(toggleElement);
elementToCheck.WaitUntilClickable(TestConstants.TenSecondsTimeout);
if (checkParent)
{
elementToCheck = elementToCheck?.Parent;
}
ToggleButton? toggleButton = elementToCheck?.AsToggleButton();
return toggleButton?.ToggleState == ToggleState.On;
}
public static T ExpandItem(this T desiredElement) where T : Element
{
AutomationElement? element = WaitUntilExists(desiredElement);
element?.Patterns.ExpandCollapse.Pattern.Expand();
return desiredElement;
}
public static T Invoke(this T desiredElement, TimeSpan? clickableTimeout = null) where T : Element
{
clickableTimeout ??= TestConstants.DefaultElementWaitingTime;
AutomationElement? elementToClick = WaitUntilExists(desiredElement);
elementToClick?.WaitUntilClickable(clickableTimeout);
elementToClick?.AsButton().Invoke();
return desiredElement;
}
public static T Hover(this T desiredElement) where T : Element
{
AutomationElement? elementToHover = WaitUntilExists(desiredElement, TestConstants.EighteenSecondsTimeout);
elementToHover?.WaitUntilClickable(TestConstants.EighteenSecondsTimeout);
elementToHover?.HoverSmart();
return desiredElement;
}
public static T TextBoxEquals(this T desiredElement, string text) where T : Element
{
AutomationElement? element = WaitUntilExists(desiredElement);
string? elementText = element?.AsTextBox().Text;
Assert.That(elementText?.Equals(text), Is.True, $"Expected string: {text} But was: {elementText}");
return desiredElement;
}
public static string? GetAutomationElementName(this T desiredElement) where T : Element
{
AutomationElement? element = WaitUntilExists(desiredElement);
return element?.Name;
}
public static T SetText(this T desiredElement, string input) where T : Element
{
AutomationElement? elementToClick = WaitUntilExists(desiredElement);
if (elementToClick != null)
{
elementToClick.AsTextBox().Text = input;
}
return desiredElement;
}
public static T SelectCheckboxByText(this T desiredElement, string itemToSelect) where T : Element
{
foreach (AutomationElement checkbox in desiredElement.GetDescendantsByControlType(ControlType.CheckBox))
{
AutomationElement? textBlock = checkbox.FindFirstChild(cf => cf.ByControlType(ControlType.Text));
if (textBlock?.Name == itemToSelect)
{
Thread.Sleep(500);
checkbox.Click();
break;
}
}
return desiredElement;
}
public static AutomationElement? TryGetElement(this T desiredElement) where T : Element
{
try
{
return WaitUntilExists(desiredElement, TestConstants.FiveSecondsTimeout);
}
catch (TimeoutException)
{
return null;
}
}
public static AutomationElement[] GetControlType(this T desiredElement, ControlType controlType) where T : Element
{
return desiredElement.GetDescendantsByControlType(controlType) ?? Array.Empty();
}
public static List GetAllCheckboxNames(this T desiredElement) where T : Element
{
return desiredElement.GetDescendantsByControlType(ControlType.CheckBox)
.Select(element => element.FindFirstChild(cf => cf.ByControlType(ControlType.Text))!.Name)
.Where(name => !string.IsNullOrEmpty(name))
.ToList() ?? [];
}
public static List GetAllChildrenNames(this T desiredElement) where T : Element
{
return desiredElement.GetDescendantsByControlType(ControlType.Text)
.Select(element => element.Name)
.Where(name => !string.IsNullOrEmpty(name))
.ToList() ?? [];
}
public static T SelectDropdownItem(this T desiredElement, string itemToSelect, string? specificInfo = null) where T : Element
{
AutomationElement? element = WaitUntilExists(desiredElement);
ComboBox? comboBox = element.AsComboBox();
if (comboBox?.Items == null || comboBox.Items.Length == 0)
{
throw new Exception($"Dropdown '{desiredElement}' has no items.");
}
DateTime timeoutDate = DateTime.UtcNow + TestConstants.TenSecondsTimeout;
while (DateTime.UtcNow < timeoutDate)
{
Keyboard.Type(FlaUI.Core.WindowsAPI.VirtualKeyShort.NEXT);
Thread.Sleep(TestConstants.NavigationDelay);
ComboBoxItem[]? items = comboBox.Items;
bool isItemInView = items.Any(item => item.FindAllDescendants(cf => cf.ByControlType(ControlType.Text)).Select(t => t.Name).Any(t => t == itemToSelect));
if (isItemInView)
{
foreach (ComboBoxItem item in items)
{
IEnumerable textNames = item.FindAllDescendants(cf => cf.ByControlType(ControlType.Text)).Select(t => t.Name);
bool primaryMatch = textNames.Any(t => t == itemToSelect);
bool secondaryMatch = specificInfo == null || textNames.Any(t => t == specificInfo);
if (primaryMatch && secondaryMatch)
{
while (item.IsOffscreen)
{
Keyboard.Type(FlaUI.Core.WindowsAPI.VirtualKeyShort.NEXT);
Thread.Sleep(TestConstants.NavigationDelay);
}
item.Click();
return desiredElement;
}
}
}
}
throw new Exception($"Item '{itemToSelect}' was not found in dropdown '{desiredElement}'.");
}
public static T WaitUntilItemDisplayed(this T desiredElement, int index) where T : Element
{
AutomationElement? element = WaitUntilExists(desiredElement);
AutomationElement[]? children = element?.FindAllChildren();
Assert.That(children, Is.Not.Null, "List is empty");
int resolvedIndex = index < 0 ? children!.Length + index : index;
AutomationElement item = children![resolvedIndex];
Assert.That(item.IsEnabled, $"Item at index {index} in '{desiredElement.SelectorName}' is not displayed.");
return desiredElement;
}
public static T ClickItem(this T desiredElement, int index) where T : Element
{
AutomationElement? element = WaitUntilExists(desiredElement);
element?.FindChildAt(index)?.Click();
return desiredElement;
}
public static T FindChild(this T desiredElement, Element childSelector) where T : Element
{
desiredElement.ChildElement = childSelector;
return desiredElement;
}
public static T And(this T desiredElement, Element otherSelector) where T : Element
{
Func originalCondition = desiredElement.Condition;
desiredElement.Condition = cf => originalCondition(cf).And(otherSelector.Condition(cf));
desiredElement.SelectorName += $" AND {otherSelector.SelectorName}";
return desiredElement;
}
public static T FindDescendant(this T desiredElement, Element descendantSelector) where T : Element
{
desiredElement.ChildElement = descendantSelector;
desiredElement.ChildElement.UseDescendantSearch = true;
return desiredElement;
}
public static void ResizeAndRepositionWindow()
{
Window appWindow = BaseTest.Window!;
ITransformPattern transformPattern = appWindow.Patterns.Transform.Pattern;
Rectangle rect = appWindow.BoundingRectangle;
Point start = new(rect.Right - 5, rect.Bottom - 5);
Point end = new(rect.Left + 50, rect.Top + 50);
Mouse.MoveTo(start);
Mouse.Down();
Mouse.MoveTo(end);
Thread.Sleep(100);
Mouse.Up();
transformPattern.Move(700, 10);
}
public static (Point Position, Size Size) GetWindowSizeAndPosition()
{
//give it time to stabilize the position
Thread.Sleep(TestConstants.TwoSecondsTimeout);
Window appWindow = BaseTest.Window!;
Rectangle windowBoundingRectangle = appWindow.BoundingRectangle;
Point windowPosition = new(windowBoundingRectangle.X, windowBoundingRectangle.Y);
Size windowSize = new(windowBoundingRectangle.Width, windowBoundingRectangle.Height);
return (windowPosition, windowSize);
}
public static T ClearInput(this T desiredElement) where T : Element
{
AutomationElement? element = WaitUntilExists(desiredElement);
if (element != null)
{
element.AsTextBox().Text = "";
}
return desiredElement;
}
public static T ClearSearch(this T desiredElement) where T : Element
{
AutomationElement? element = WaitUntilExists(desiredElement);
Rectangle rect = element!.Parent.BoundingRectangle;
int x = rect.Right - (rect.Height / 2);
int y = rect.Top + rect.Height / 2;
Mouse.Click(new Point(x, y));
return desiredElement;
}
public static AutomationElement[] FindAllElements(this T desiredElement) where T : Element
{
WaitUntilExists(desiredElement);
return BaseTest.Window?.FindAllDescendants(desiredElement.Condition) ?? [];
}
public static Element ScrollIntoView(this T desiredElement) where T : Element
{
AutomationElement? element = WaitUntilExists(desiredElement);
element?.Patterns.ScrollItem.Pattern.ScrollIntoView();
return desiredElement;
}
public static Element Scroll(this T desiredElement, int verticalPercent = 0, int horizontalPercent = 0) where T : Element
{
AutomationElement? element = WaitUntilExists(desiredElement);
IScrollPattern? scrollPattern = element?.Patterns.Scroll.PatternOrDefault;
if (scrollPattern != null)
{
double h = scrollPattern.HorizontallyScrollable ? horizontalPercent : scrollPattern.HorizontalScrollPercent;
double v = scrollPattern.VerticallyScrollable ? verticalPercent : scrollPattern.VerticalScrollPercent;
scrollPattern.SetScrollPercent(h, v);
}
return desiredElement;
}
public static Element TextEquals(this T desiredElement, string text) where T : Element
{
AutomationElement? element = WaitUntilExists(desiredElement);
string? elementText = element?.AsLabel().Text;
Assert.That(elementText?.Equals(text), Is.True, $"Expected string: {text} But was: {elementText}");
return desiredElement;
}
public static Element TextContains(this T desiredElement, string text) where T : Element
{
AutomationElement? element = WaitUntilExists(desiredElement);
string? elementText = element?.AsLabel().Text;
if (elementText?.Contains(text) != true)
{
throw new AssertionException($"Expected string: {text} But was: {elementText}");
}
return desiredElement;
}
public static Element TextContainsOneOf(this T desiredElement, List texts) where T : Element
{
AutomationElement? element = WaitUntilExists(desiredElement);
string? elementText = element?.AsLabel().Text;
Assert.That(texts.Any(oneOfText => elementText?.Contains(oneOfText) == true), Is.True, $"Expected string to contain at least one of: {string.Join(", ", texts)}, but was: {elementText}");
return desiredElement;
}
public static Element ValueEquals(this T desiredElement, string value) where T : Element
{
AutomationElement? element = WaitUntilExists(desiredElement);
string? elementValue = element?.Patterns.Value.Pattern.Value;
Assert.That(elementValue?.Equals(value), Is.True, $"Expected value: {value} But was: {elementValue}");
return desiredElement;
}
public static Element ComboBoxSelectedEquals(this T desiredElement, string expectedValue) where T : Element
{
AutomationElement? element = WaitUntilExists(desiredElement);
ISelectionPattern? selectionPattern = element?.Patterns.Selection.PatternOrDefault;
if (selectionPattern != null)
{
AutomationElement[]? selectedItems = selectionPattern.Selection.Value;
if (selectedItems != null && selectedItems.Length > 0)
{
AutomationElement selectedItem = selectedItems[0];
AutomationElement? textBlock = selectedItem.FindFirstDescendant(cf => cf.ByControlType(FlaUI.Core.Definitions.ControlType.Text));
if (textBlock != null)
{
string? displayText = textBlock.Name;
Assert.That(displayText?.Equals(expectedValue), Is.True,
$"Expected ComboBox selected value: '{expectedValue}' But was: '{displayText}'");
return desiredElement;
}
}
}
Assert.Fail($"Could not verify ComboBox selection. Expected value: '{expectedValue}'");
return desiredElement;
}
public static AutomationElement? WaitUntilExists(this T desiredElement, TimeSpan? time = null, TimeSpan? retryIntervalOverload = null) where T : Element
{
return WaitForElement(desiredElement, time, element => element != null, null, retryIntervalOverload);
}
public static AutomationElement? WaitUntilDisplayed(this T desiredElement, TimeSpan? time = null, TimeSpan? retryIntervalOverload = null) where T : Element
{
return WaitForElement(desiredElement, time, element => element != null && !element.IsOffscreen, null, retryIntervalOverload);
}
public static void DoesNotExist(this T desiredElement) where T : Element
{
AutomationElement? element = FindFirstDescendantUsingChildren(desiredElement.Condition);
if (desiredElement.ChildElement != null && element != null)
{
element = desiredElement.ChildElement.UseDescendantSearch
? element.FindFirstDescendant(desiredElement.ChildElement.Condition)
: element.FindFirstChild(desiredElement.ChildElement.Condition);
Assert.That(element, Is.Null, $"Element {desiredElement.ChildElement.SelectorName} inside {desiredElement.SelectorName} was found. But it should not exist.");
}
else
{
Assert.That(element, Is.Null, $"Element {desiredElement.SelectorName} was found. But it should not exist.");
}
}
public static void AssertIsFocused(this T desiredElement, bool expected = true) where T : Element
{
AutomationElement? element = WaitUntilExists(desiredElement);
Assert.That(element?.Properties.HasKeyboardFocus.Value, expected ? Is.True : Is.False);
}
private static AutomationElement[] GetDescendantsByControlType(this T desiredElement, ControlType controlType) where T : Element
{
AutomationElement? element = WaitUntilExists(desiredElement);
return element?.FindAllDescendants(cf => cf.ByControlType(controlType)) ?? [];
}
private static AutomationElement? WaitForElement(
T desiredElement,
TimeSpan? time,
Func condition,
string? customMessage = null,
TimeSpan? retryIntervalOverload = null) where T : Element
{
time ??= TestConstants.DefaultElementWaitingTime;
TimeSpan retryInterval = retryIntervalOverload ?? TestConstants.RetryInterval;
AutomationElement? elementToWaitFor = null;
RetryResult retry = Retry.WhileFalse(
() =>
{
try
{
BaseTest.RefreshWindow();
BaseTest.App?.WaitWhileBusy();
elementToWaitFor = Element.Root != null
? Element.Root.FindFirstDescendant(desiredElement.Condition)
: FindFirstDescendantUsingChildren(desiredElement.Condition);
if (desiredElement.ChildElement != null && elementToWaitFor != null)
{
elementToWaitFor = desiredElement.ChildElement.UseDescendantSearch
? elementToWaitFor.FindFirstDescendant(desiredElement.ChildElement.Condition)
: elementToWaitFor.FindFirstChild(desiredElement.ChildElement.Condition);
}
return condition(elementToWaitFor);
}
catch (COMException)
{
// Sometimes framework throws these exceptions when searching for elements.
// Ignoring it, does not introduce side effects and increases stability.
return false;
}
},
time, retryInterval);
if (!retry.Success)
{
string errorMessage = customMessage ??
(desiredElement.ChildElement != null
? $"Failed to get child element {desiredElement.ChildElement.SelectorName} inside {desiredElement.SelectorName} element within {time?.TotalSeconds} seconds."
: $"Failed to get {desiredElement.SelectorName} element within {time?.TotalSeconds} seconds.");
throw new TimeoutException(errorMessage);
}
return elementToWaitFor;
}
private static AutomationElement? FindFirstDescendantUsingChildren(Func conditionFunc)
{
AutomationElement? child = BaseTest.Window?.FindFirstChild(conditionFunc);
if (child != null)
{
return child;
}
AutomationElement[]? children = BaseTest.Window?.FindAllChildren() ?? [];
foreach (AutomationElement windowChild in children)
{
AutomationElement? descendant = windowChild.FindFirstDescendant(conditionFunc);
if (descendant != null)
{
return descendant;
}
}
return null;
}
public static bool IsChecked(this T desiredElement) where T : Element
{
AutomationElement? element = WaitUntilExists(desiredElement);
if (element is null)
{
return false;
}
RadioButton radioButton = new(element.FrameworkAutomationElement);
return radioButton.IsChecked;
}
public static T ClickTabByName(this T desiredElement, string partialName) where T : Element
{
AutomationElement? element = WaitUntilExists(desiredElement);
AutomationElement? tabItem = element?
.FindAllDescendants(cf => cf.ByControlType(ControlType.TabItem))
.FirstOrDefault(t => t.Name.Contains(partialName, StringComparison.OrdinalIgnoreCase))
?? throw new Exception($"TabItem containing '{partialName}' not found.");
tabItem.AsTabItem().Select();
return desiredElement;
}
}