/* * 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; } }