diff --git a/.gitignore b/.gitignore
index 3ba5f170..dc8f0c02 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,6 +7,7 @@
*~
.idea
.vscode
+.vs
# Test files
godog.test
@@ -35,6 +36,8 @@ cmd/Import-Export/deploy
proton-bridge
cmd/Desktop-Bridge/*.exe
cmd/launcher/*.exe
+bin/
+obj/
# Jetbrains (CLion, Golang) cmake build dirs
cmake-build-*/
diff --git a/tests/e2e/ui_tests/windows_os/ProtonMailBridge.UI.Tests.csproj b/tests/e2e/ui_tests/windows_os/ProtonMailBridge.UI.Tests.csproj
new file mode 100644
index 00000000..30fee0b2
--- /dev/null
+++ b/tests/e2e/ui_tests/windows_os/ProtonMailBridge.UI.Tests.csproj
@@ -0,0 +1,35 @@
+
+
+
+ net8.0-windows7.0
+ enable
+ enable
+
+ false
+ true
+ x64
+ AnyCPU;x64
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/e2e/ui_tests/windows_os/Results/HomeResult.cs b/tests/e2e/ui_tests/windows_os/Results/HomeResult.cs
new file mode 100644
index 00000000..e3d4bd47
--- /dev/null
+++ b/tests/e2e/ui_tests/windows_os/Results/HomeResult.cs
@@ -0,0 +1,29 @@
+using FlaUI.Core.AutomationElements;
+using FlaUI.Core.Definitions;
+
+namespace ProtonMailBridge.UI.Tests.Results
+{
+ public class HomeResult : UIActions
+ {
+ private Button SignOutButton => AccountView.FindFirstDescendant(cf => cf.ByControlType(ControlType.Button).And(cf.ByName("Sign out"))).AsButton();
+ private AutomationElement NotificationWindow => Window.FindFirstDescendant(cf => cf.ByControlType(ControlType.Window));
+ private TextBox FreeAccountErrorText => NotificationWindow.FindFirstDescendant(cf => cf.ByControlType(ControlType.Text)).AsTextBox();
+ private TextBox SignedOutAccount => AccountView.FindFirstDescendant(cf => cf.ByControlType(ControlType.Text)).AsTextBox();
+ public HomeResult CheckIfLoggedIn()
+ {
+ Assert.That(SignOutButton.IsAvailable, Is.True);
+ return this;
+ }
+
+ public HomeResult CheckIfFreeAccountErrorIsDisplayed(string ErrorText)
+ {
+ Assert.That(FreeAccountErrorText.Name == ErrorText, Is.True);
+ return this;
+ }
+ public HomeResult CheckIfAccountIsSignedOut()
+ {
+ Assert.That(SignedOutAccount.IsAvailable, Is.True);
+ return this;
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/e2e/ui_tests/windows_os/TestSession.cs b/tests/e2e/ui_tests/windows_os/TestSession.cs
new file mode 100644
index 00000000..a79ed274
--- /dev/null
+++ b/tests/e2e/ui_tests/windows_os/TestSession.cs
@@ -0,0 +1,43 @@
+using System;
+using System.Threading;
+using FlaUI.Core.AutomationElements;
+using FlaUI.Core;
+using FlaUI.UIA3;
+using ProtonMailBridge.UI.Tests.TestsHelper;
+using FlaUI.Core.Input;
+
+namespace ProtonMailBridge.UI.Tests
+{
+ public class TestSession
+ {
+
+ public static Application App;
+ protected static Application Service;
+ protected static Window Window;
+
+ protected static void ClientCleanup()
+ {
+ App.Kill();
+ App.Dispose();
+ // Give some time to properly exit the app
+ Thread.Sleep(2000);
+ }
+
+ public static void LaunchApp()
+ {
+ string appExecutable = TestData.AppExecutable;
+ Application.Launch(appExecutable);
+ Wait.UntilInputIsProcessed(TestData.FiveSecondsTimeout);
+ App = Application.Attach("bridge-gui.exe");
+
+ try
+ {
+ Window = App.GetMainWindow(new UIA3Automation(), TestData.ThirtySecondsTimeout);
+ }
+ catch (System.TimeoutException)
+ {
+ Assert.Fail("Failed to get window of application!");
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/e2e/ui_tests/windows_os/Tests/LoginLogoutTests.cs b/tests/e2e/ui_tests/windows_os/Tests/LoginLogoutTests.cs
new file mode 100644
index 00000000..23ffe8cb
--- /dev/null
+++ b/tests/e2e/ui_tests/windows_os/Tests/LoginLogoutTests.cs
@@ -0,0 +1,51 @@
+using NUnit.Framework;
+using ProtonMailBridge.UI.Tests.TestsHelper;
+using ProtonMailBridge.UI.Tests.Windows;
+using ProtonMailBridge.UI.Tests.Results;
+
+namespace ProtonMailBridge.UI.Tests.Tests
+{
+ [TestFixture]
+ public class LoginLogoutTests : TestSession
+ {
+ private readonly LoginWindow _loginWindow = new();
+ private readonly HomeWindow _mainWindow = new();
+ private readonly HomeResult _homeResult = new();
+ private readonly string FreeAccountErrorText = "Bridge is exclusive to our mail paid plans. Upgrade your account to use Bridge.";
+
+ [Test]
+ public void LoginAsPaidUser()
+ {
+ _loginWindow.SignIn(TestUserData.GetPaidUser());
+ _homeResult.CheckIfLoggedIn();
+ }
+
+ [Test]
+ public void LoginAsFreeUser()
+ {
+ _loginWindow.SignIn(TestUserData.GetFreeUser());
+ _homeResult.CheckIfFreeAccountErrorIsDisplayed(FreeAccountErrorText);
+ }
+
+ [Test]
+ public void SuccessfullLogout()
+ {
+ _loginWindow.SignIn(TestUserData.GetPaidUser());
+ _mainWindow.SignOutAccount();
+ _homeResult.CheckIfAccountIsSignedOut();
+ }
+
+ [SetUp]
+ public void TestInitialize()
+ {
+ LaunchApp();
+ }
+
+ [TearDown]
+ public void TestCleanup()
+ {
+ _mainWindow.RemoveAccount();
+ ClientCleanup();
+ }
+ }
+}
diff --git a/tests/e2e/ui_tests/windows_os/TestsHelper/TestData.cs b/tests/e2e/ui_tests/windows_os/TestsHelper/TestData.cs
new file mode 100644
index 00000000..b7a4fae0
--- /dev/null
+++ b/tests/e2e/ui_tests/windows_os/TestsHelper/TestData.cs
@@ -0,0 +1,16 @@
+using System;
+using System.Linq;
+using System.IO;
+
+namespace ProtonMailBridge.UI.Tests.TestsHelper
+{
+ public static class TestData
+ {
+ public static TimeSpan FiveSecondsTimeout => TimeSpan.FromSeconds(5);
+ public static TimeSpan TenSecondsTimeout => TimeSpan.FromSeconds(10);
+ public static TimeSpan ThirtySecondsTimeout => TimeSpan.FromSeconds(30);
+ public static TimeSpan OneMinuteTimeout => TimeSpan.FromSeconds(60);
+ public static TimeSpan RetryInterval => TimeSpan.FromMilliseconds(1000);
+ public static string AppExecutable => "C:\\Program Files\\Proton AG\\Proton Mail Bridge\\bridge-gui.exe";
+ }
+}
diff --git a/tests/e2e/ui_tests/windows_os/TestsHelper/TestUserData.cs b/tests/e2e/ui_tests/windows_os/TestsHelper/TestUserData.cs
new file mode 100644
index 00000000..b34fa5d4
--- /dev/null
+++ b/tests/e2e/ui_tests/windows_os/TestsHelper/TestUserData.cs
@@ -0,0 +1,58 @@
+using System;
+
+namespace ProtonMailBridge.UI.Tests.TestsHelper
+{
+ public class TestUserData
+ {
+ public string Username { get; set; }
+ public string Password { get; set; }
+
+ public TestUserData(string username, string password)
+ {
+ Username = username;
+ Password = password;
+ }
+
+ public static TestUserData GetFreeUser()
+ {
+ (string username, string password) = GetusernameAndPassword("BRIDGE_FLAUI_FREE_USER");
+ return new TestUserData(username, password);
+ }
+
+ public static TestUserData GetPaidUser()
+ {
+ (string username, string password) = GetusernameAndPassword("BRIDGE_FLAUI_PAID_USER");
+ return new TestUserData(username, password);
+ }
+
+ public static TestUserData GetIncorrectCredentialsUser()
+ {
+ return new TestUserData("IncorrectUsername", "IncorrectPass");
+ }
+
+ private static (string, string) GetusernameAndPassword(string userType)
+ {
+ // Get the environment variable for the user and check if missing
+ // When changing or adding an environment variable, you must restart Visual Studio
+ // if you have it open while doing this
+ string? str = Environment.GetEnvironmentVariable(userType);
+ if (string.IsNullOrEmpty(str))
+ {
+ throw new Exception($"Missing environment variable: {userType}");
+ }
+
+ // Check if the environment variable contains only one ':'
+ // The ':' character must be between the username/email and password
+ string ch = ":";
+ if ((str.IndexOf(ch) != str.LastIndexOf(ch)) | (str.IndexOf(ch) == -1))
+ {
+ throw new Exception(
+ $"Environment variable {str} must contain one ':' and it must be between username and password!"
+ );
+ }
+
+ string[] split = str.Split(':');
+ return (split[0], split[1]);
+ }
+ }
+}
diff --git a/tests/e2e/ui_tests/windows_os/UIActions.cs b/tests/e2e/ui_tests/windows_os/UIActions.cs
new file mode 100644
index 00000000..5adbbdda
--- /dev/null
+++ b/tests/e2e/ui_tests/windows_os/UIActions.cs
@@ -0,0 +1,14 @@
+using System;
+using FlaUI.Core.AutomationElements;
+using FlaUI.Core.Definitions;
+using FlaUI.Core.Input;
+using FlaUI.Core.Tools;
+using NUnit.Framework;
+
+namespace ProtonMailBridge.UI.Tests
+{
+ public class UIActions : TestSession
+ {
+ public AutomationElement AccountView => Window.FindFirstDescendant(cf => cf.ByControlType(ControlType.Pane));
+ }
+}
\ No newline at end of file
diff --git a/tests/e2e/ui_tests/windows_os/Windows/HomeWindow.cs b/tests/e2e/ui_tests/windows_os/Windows/HomeWindow.cs
new file mode 100644
index 00000000..bcc39f04
--- /dev/null
+++ b/tests/e2e/ui_tests/windows_os/Windows/HomeWindow.cs
@@ -0,0 +1,34 @@
+using FlaUI.Core.AutomationElements;
+using FlaUI.Core.Definitions;
+using System;
+
+
+namespace ProtonMailBridge.UI.Tests.Windows
+{
+ public class HomeWindow : UIActions
+ {
+ private AutomationElement[] AccountViewButtons => AccountView.FindAllChildren(cf => cf.ByControlType(ControlType.Button));
+ private Button RemoveAccountButton => AccountViewButtons[1].AsButton();
+ private AutomationElement RemoveAccountConfirmModal => Window.FindFirstDescendant(cf => cf.ByControlType(ControlType.Window));
+ private Button ConfirmRemoveAccountButton => RemoveAccountConfirmModal.FindFirstDescendant(cf => cf.ByControlType(ControlType.Button).And(cf.ByName("Remove this account"))).AsButton();
+ private Button SignOutButton => AccountView.FindFirstDescendant(cf => cf.ByControlType(ControlType.Button).And(cf.ByName("Sign out"))).AsButton();
+ public HomeWindow RemoveAccount()
+ {
+ try
+ {
+ RemoveAccountButton.Click();
+ ConfirmRemoveAccountButton.Click();
+ }
+ catch (System.NullReferenceException)
+ {
+ ClientCleanup();
+ }
+ return this;
+ }
+ public HomeWindow SignOutAccount()
+ {
+ SignOutButton.Click();
+ return this;
+ }
+ }
+}
diff --git a/tests/e2e/ui_tests/windows_os/Windows/LoginWindow.cs b/tests/e2e/ui_tests/windows_os/Windows/LoginWindow.cs
new file mode 100644
index 00000000..604f290a
--- /dev/null
+++ b/tests/e2e/ui_tests/windows_os/Windows/LoginWindow.cs
@@ -0,0 +1,49 @@
+using FlaUI.Core.AutomationElements;
+using FlaUI.Core.Input;
+using FlaUI.Core.Definitions;
+using ProtonMailBridge.UI.Tests.TestsHelper;
+
+namespace ProtonMailBridge.UI.Tests.Windows
+{
+ public class LoginWindow : UIActions
+ {
+ private AutomationElement[] InputFields => Window.FindAllDescendants(cf => cf.ByControlType(ControlType.Edit));
+ private TextBox UsernameInput => InputFields[0].AsTextBox();
+ private TextBox PasswordInput => InputFields[1].AsTextBox();
+ private Button SignInButton => Window.FindFirstDescendant(cf => cf.ByControlType(ControlType.Button).And(cf.ByName("Sign in"))).AsButton();
+ private Button StartSetupButton => Window.FindFirstDescendant(cf => cf.ByName("Start setup")).AsButton();
+ private Button SetUpLater => Window.FindFirstDescendant(cf => cf.ByName("Setup later")).AsButton();
+
+ public LoginWindow SignIn(TestUserData user)
+ {
+ ClickStartSetupButton();
+ EnterCredentials(user);
+ Wait.UntilInputIsProcessed(TestData.TenSecondsTimeout);
+ SetUpLater?.Click();
+
+ return this;
+ }
+
+ public LoginWindow SignIn(string username, string password)
+ {
+ TestUserData user = new TestUserData(username, password);
+ SignIn(user);
+ return this;
+ }
+
+ public LoginWindow ClickStartSetupButton()
+ {
+ StartSetupButton?.Click();
+
+ return this;
+ }
+
+ public LoginWindow EnterCredentials(TestUserData user)
+ {
+ UsernameInput.Text = user.Username;
+ PasswordInput.Text = user.Password;
+ SignInButton.Click();
+ return this;
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/e2e/ui_tests/windows_os/app.config b/tests/e2e/ui_tests/windows_os/app.config
new file mode 100644
index 00000000..3c1fd93a
--- /dev/null
+++ b/tests/e2e/ui_tests/windows_os/app.config
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/e2e/ui_tests/windows_os/ui_tests.sln b/tests/e2e/ui_tests/windows_os/ui_tests.sln
new file mode 100644
index 00000000..6a4690f7
--- /dev/null
+++ b/tests/e2e/ui_tests/windows_os/ui_tests.sln
@@ -0,0 +1,31 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.11.35208.52
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProtonMailBridge.UI.Tests", "ProtonMailBridge.UI.Tests.csproj", "{027E5266-E353-4095-AF24-B3ED240EACAA}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Debug|x64 = Debug|x64
+ Release|Any CPU = Release|Any CPU
+ Release|x64 = Release|x64
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {027E5266-E353-4095-AF24-B3ED240EACAA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {027E5266-E353-4095-AF24-B3ED240EACAA}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {027E5266-E353-4095-AF24-B3ED240EACAA}.Debug|x64.ActiveCfg = Debug|x64
+ {027E5266-E353-4095-AF24-B3ED240EACAA}.Debug|x64.Build.0 = Debug|x64
+ {027E5266-E353-4095-AF24-B3ED240EACAA}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {027E5266-E353-4095-AF24-B3ED240EACAA}.Release|Any CPU.Build.0 = Release|Any CPU
+ {027E5266-E353-4095-AF24-B3ED240EACAA}.Release|x64.ActiveCfg = Release|x64
+ {027E5266-E353-4095-AF24-B3ED240EACAA}.Release|x64.Build.0 = Release|x64
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {817DD45A-EA2C-4F16-A680-5810DADCE4E7}
+ EndGlobalSection
+EndGlobal