diff --git a/src/OrchardCoreContrib.Testing.UI/PageObjects/AdminPage.cs b/src/OrchardCoreContrib.Testing.UI/PageObjects/AdminPage.cs new file mode 100644 index 0000000..6dd45ee --- /dev/null +++ b/src/OrchardCoreContrib.Testing.UI/PageObjects/AdminPage.cs @@ -0,0 +1,42 @@ +using Microsoft.Playwright; +using OrchardCoreContrib.Testing.UI.Helpers; + +namespace OrchardCoreContrib.Testing.UI.PageObjects; + +/// +/// Represents a base class for admin page objects. +/// +public abstract class AdminPage : PageBase +{ + /// + public abstract override string Slug { get; } + + /// + /// Changes the theme. + /// + /// The theme mode to be applied. + public async Task ChangeThemeAsync(ThemeMode themeMode) + { + await Page.FindElement(By.Id("bd-theme")).ClickAsync(); + await Page.FindElement(By.Attribute("data-bs-theme-value", themeMode.ToString().ToLower(), "button")).ClickAsync(); + } + + /// + /// Navigates to the profile page. + /// + public async Task GoToProfilePage() => await PageFactory.CreateAsync(); + + /// + /// Navigates to the change password page. + /// + public async Task GoToChangePasswordPage() => await PageFactory.CreateAsync(); + + /// + /// Log out the current user. + /// + public async Task LogoutAsync() + { + await Page.InnerPage.GetByRole(AriaRole.Link, new() { Name = "admin" }).ClickAsync(); + await Page.InnerPage.GetByRole(AriaRole.Button, new() { Name = "Log off" }).ClickAsync(); + } +} diff --git a/src/OrchardCoreContrib.Testing.UI/PageObjects/ChangePasswordPage.cs b/src/OrchardCoreContrib.Testing.UI/PageObjects/ChangePasswordPage.cs new file mode 100644 index 0000000..f99301b --- /dev/null +++ b/src/OrchardCoreContrib.Testing.UI/PageObjects/ChangePasswordPage.cs @@ -0,0 +1,27 @@ +using OrchardCoreContrib.Testing.UI.Helpers; + +namespace OrchardCoreContrib.Testing.UI.PageObjects; + +/// +/// Represents a change password page. +/// +public class ChangePasswordPage : AdminPage +{ + /// + public override string Slug => "ChangePassword"; + + /// + /// Changes the user password. + /// + /// The current password. + /// The new password. + public async Task ChangeAsync(string currentPassword, string newPassword) + { + await Page.FindElement(By.Attribute("name", "CurrentPassword")).TypeAsync(currentPassword); + await Page.FindElement(By.Attribute("name", "Password")).TypeAsync(newPassword); + await Page.FindElement(By.Attribute("name", "PasswordConfirmation")).TypeAsync(newPassword); + await Page.FindElement(By.Attribute("type", "submit", "button")).ClickAsync(); + + return Page.Content.Contains("Your password has been changed successfully."); + } +} diff --git a/src/OrchardCoreContrib.Testing.UI/PageObjects/DashboardPage.cs b/src/OrchardCoreContrib.Testing.UI/PageObjects/DashboardPage.cs new file mode 100644 index 0000000..45d1311 --- /dev/null +++ b/src/OrchardCoreContrib.Testing.UI/PageObjects/DashboardPage.cs @@ -0,0 +1,10 @@ +namespace OrchardCoreContrib.Testing.UI.PageObjects; + +/// +/// Represents the dashboard page. +/// +public class DashboardPage : AdminPage +{ + /// + public override string Slug => "Admin/"; +} diff --git a/src/OrchardCoreContrib.Testing.UI/PageObjects/LoginPage.cs b/src/OrchardCoreContrib.Testing.UI/PageObjects/LoginPage.cs new file mode 100644 index 0000000..ea5d88d --- /dev/null +++ b/src/OrchardCoreContrib.Testing.UI/PageObjects/LoginPage.cs @@ -0,0 +1,31 @@ +using Microsoft.Playwright; +using OrchardCoreContrib.Testing.UI.Helpers; + +namespace OrchardCoreContrib.Testing.UI.PageObjects; + +/// +/// Represents a login page. +/// +public class LoginPage : PageBase +{ + /// + public override string Slug => "Login"; + + /// + /// Logs in with the specified username and password. + /// + /// The user name. + /// The password. + public async Task LoginAsync(string username, string password) + { + await Page.FindElement(By.Attribute("name", "UserName")).TypeAsync(username); + await Page.FindElement(By.Attribute("name", "Password")).TypeAsync(password); + await Page.FindElement(By.Attribute("type", "submit")).ClickAsync(); + + var isAuthenticated = await Page.InnerPage + .GetByRole(AriaRole.Link, new() { Name = username }) + .IsVisibleAsync(); + + return isAuthenticated; + } +} diff --git a/src/OrchardCoreContrib.Testing.UI/PageObjects/PageBase.cs b/src/OrchardCoreContrib.Testing.UI/PageObjects/PageBase.cs new file mode 100644 index 0000000..fa0a4b0 --- /dev/null +++ b/src/OrchardCoreContrib.Testing.UI/PageObjects/PageBase.cs @@ -0,0 +1,26 @@ +namespace OrchardCoreContrib.Testing.UI.PageObjects; + +/// +/// Represents a base class for page objects. +/// +public abstract class PageBase +{ + /// + /// Gets the underlying object. + /// + public IPage Page { get; internal set; } + + /// + /// Gets the slug of the page. + /// + public abstract string Slug { get; } + + internal string BaseUrl { get; set; } + + internal async Task GoToAsync() + { + await Page.GoToAsync(BaseUrl + Slug); + + return this; + } +} diff --git a/src/OrchardCoreContrib.Testing.UI/PageObjects/PageFactory.cs b/src/OrchardCoreContrib.Testing.UI/PageObjects/PageFactory.cs new file mode 100644 index 0000000..d46ecc9 --- /dev/null +++ b/src/OrchardCoreContrib.Testing.UI/PageObjects/PageFactory.cs @@ -0,0 +1,37 @@ +namespace OrchardCoreContrib.Testing.UI.PageObjects; + +/// +/// Represents a factory for creating page objects. +/// +public class PageFactory +{ + private static IPage _page; + private static string _baseUrl; + + /// + /// Initializes the page factory. + /// + /// The . + /// The base URL. + /// + public static async Task InitializeAsync(IBrowser browser, string baseUrl) + { + _page = await browser.OpenPageAsync(baseUrl); + + _baseUrl = baseUrl; + } + + /// + /// Creates a page object of the specified type. + /// + /// The page type. + public static async Task CreateAsync() where TPage : PageBase + { + var page = Activator.CreateInstance(); + + page.Page = _page; + page.BaseUrl = _baseUrl; + + return (TPage)await page.GoToAsync(); + } +} diff --git a/src/OrchardCoreContrib.Testing.UI/PageObjects/ProfilePage.cs b/src/OrchardCoreContrib.Testing.UI/PageObjects/ProfilePage.cs new file mode 100644 index 0000000..46a55ff --- /dev/null +++ b/src/OrchardCoreContrib.Testing.UI/PageObjects/ProfilePage.cs @@ -0,0 +1,17 @@ +namespace OrchardCoreContrib.Testing.UI.PageObjects; + +/// +/// Represents the profile page. +/// +public class ProfilePage : AdminPage +{ + /// + public override string Slug => "Admin/Users/Edit"; + + /// + /// Changes the user profile information. + /// + /// The phone number. + /// The user reoles. + public Task ChangeAsync(string phoneNumber, params string[] roleNames) => throw new NotImplementedException(); +} diff --git a/src/OrchardCoreContrib.Testing.UI/PageObjects/ThemeMode.cs b/src/OrchardCoreContrib.Testing.UI/PageObjects/ThemeMode.cs new file mode 100644 index 0000000..311f446 --- /dev/null +++ b/src/OrchardCoreContrib.Testing.UI/PageObjects/ThemeMode.cs @@ -0,0 +1,20 @@ +namespace OrchardCoreContrib.Testing.UI.PageObjects; + +/// +/// Defines the theme modes. +/// +public enum ThemeMode +{ + /// + /// The theme mode will set automatically based on the windows theme. + /// + Auto, + /// + /// The light theme mode. + /// + Light, + /// + /// The dark theme mode. + /// + Dark +} diff --git a/src/OrchardCoreContrib.Testing.UI/UITestOfT.cs b/src/OrchardCoreContrib.Testing.UI/UITestOfT.cs index 7bc998d..094430f 100644 --- a/src/OrchardCoreContrib.Testing.UI/UITestOfT.cs +++ b/src/OrchardCoreContrib.Testing.UI/UITestOfT.cs @@ -1,4 +1,5 @@ using OrchardCoreContrib.Testing.UI.Infrastructure; +using OrchardCoreContrib.Testing.UI.PageObjects; namespace OrchardCoreContrib.Testing.UI; @@ -17,4 +18,11 @@ public class UITest(BrowserType browserType = BrowserType.Edge, bool h Delay = delay }), IUITest where TStartup : class { + /// + public override async Task InitializeAsync() + { + await base.InitializeAsync(); + + await PageFactory.InitializeAsync(Browser, BaseUrl); + } } diff --git a/test/OrchardCoreContrib.Testing.UI.Tests/PageObjects/PageFactoryTests.cs b/test/OrchardCoreContrib.Testing.UI.Tests/PageObjects/PageFactoryTests.cs new file mode 100644 index 0000000..2313293 --- /dev/null +++ b/test/OrchardCoreContrib.Testing.UI.Tests/PageObjects/PageFactoryTests.cs @@ -0,0 +1,29 @@ +namespace OrchardCoreContrib.Testing.UI.PageObjects.Tests; + +public class PageFactoryTests +{ + [Fact] + public async Task CreatePage() + { + // Arrange + var baseUrl = "https://localhost:8080"; + var browserMock = new Mock(); + browserMock.Setup(b => b.OpenPageAsync(It.IsAny())) + .ReturnsAsync(() => new Page(new PlaywrightPageAccessor(Mock.Of()), browserMock.Object)); + + await PageFactory.InitializeAsync(browserMock.Object, baseUrl); + + // Act + var page = await PageFactory.CreateAsync(); + + // Assert + Assert.NotNull(page.Page); + Assert.Equal(baseUrl, page.BaseUrl); + Assert.Equal("/my-page", page.Slug); + } + + public class MyPage : PageBase + { + public override string Slug => "/my-page"; + } +}