From d2a67deafa3adc0106b5383e1f66044c3ccf11e6 Mon Sep 17 00:00:00 2001 From: Hisham Bin Ateya Date: Mon, 13 May 2024 02:14:56 +0300 Subject: [PATCH 1/3] Add Page objects models --- .../PageObjects/AdminPage.cs | 43 +++++++++++++++++++ .../PageObjects/ChangePasswordPage.cs | 28 ++++++++++++ .../PageObjects/DashboardPage.cs | 11 +++++ .../PageObjects/LoginPage.cs | 32 ++++++++++++++ .../PageObjects/PageBase.cs | 27 ++++++++++++ .../PageObjects/PageFactory.cs | 36 ++++++++++++++++ .../PageObjects/ProfilePage.cs | 18 ++++++++ .../PageObjects/ThemeMode.cs | 20 +++++++++ .../PageObjects/PageFactoryTests.cs | 29 +++++++++++++ 9 files changed, 244 insertions(+) create mode 100644 src/OrchardCoreContrib.Testing.UI/PageObjects/AdminPage.cs create mode 100644 src/OrchardCoreContrib.Testing.UI/PageObjects/ChangePasswordPage.cs create mode 100644 src/OrchardCoreContrib.Testing.UI/PageObjects/DashboardPage.cs create mode 100644 src/OrchardCoreContrib.Testing.UI/PageObjects/LoginPage.cs create mode 100644 src/OrchardCoreContrib.Testing.UI/PageObjects/PageBase.cs create mode 100644 src/OrchardCoreContrib.Testing.UI/PageObjects/PageFactory.cs create mode 100644 src/OrchardCoreContrib.Testing.UI/PageObjects/ProfilePage.cs create mode 100644 src/OrchardCoreContrib.Testing.UI/PageObjects/ThemeMode.cs create mode 100644 test/OrchardCoreContrib.Testing.UI.Tests/PageObjects/PageFactoryTests.cs diff --git a/src/OrchardCoreContrib.Testing.UI/PageObjects/AdminPage.cs b/src/OrchardCoreContrib.Testing.UI/PageObjects/AdminPage.cs new file mode 100644 index 0000000..097844c --- /dev/null +++ b/src/OrchardCoreContrib.Testing.UI/PageObjects/AdminPage.cs @@ -0,0 +1,43 @@ +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(IPage page) : PageBase(page) +{ + /// + 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..d73c761 --- /dev/null +++ b/src/OrchardCoreContrib.Testing.UI/PageObjects/ChangePasswordPage.cs @@ -0,0 +1,28 @@ +using OrchardCoreContrib.Testing.UI.Helpers; + +namespace OrchardCoreContrib.Testing.UI.PageObjects; + +/// +/// Represents a change password page. +/// +/// The . +public class ChangePasswordPage(IPage page) : AdminPage(page) +{ + /// + 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..509a06f --- /dev/null +++ b/src/OrchardCoreContrib.Testing.UI/PageObjects/DashboardPage.cs @@ -0,0 +1,11 @@ +namespace OrchardCoreContrib.Testing.UI.PageObjects; + +/// +/// Represents the dashboard page. +/// +/// The . +public class DashboardPage(IPage page) : AdminPage(page) +{ + /// + 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..4c31496 --- /dev/null +++ b/src/OrchardCoreContrib.Testing.UI/PageObjects/LoginPage.cs @@ -0,0 +1,32 @@ +using Microsoft.Playwright; +using OrchardCoreContrib.Testing.UI.Helpers; + +namespace OrchardCoreContrib.Testing.UI.PageObjects; + +/// +/// Represents a login page. +/// +/// The . +public class LoginPage(IPage page) : PageBase(page) +{ + /// + 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..b9ca1a4 --- /dev/null +++ b/src/OrchardCoreContrib.Testing.UI/PageObjects/PageBase.cs @@ -0,0 +1,27 @@ +namespace OrchardCoreContrib.Testing.UI.PageObjects; + +/// +/// Represents a base class for page objects. +/// +/// +public abstract class PageBase(IPage page) +{ + /// + /// Gets the underlying object. + /// + public IPage Page => page; + + /// + /// 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..2e38b44 --- /dev/null +++ b/src/OrchardCoreContrib.Testing.UI/PageObjects/PageFactory.cs @@ -0,0 +1,36 @@ +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 = (TPage)Activator.CreateInstance(typeof(TPage), _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..730ee57 --- /dev/null +++ b/src/OrchardCoreContrib.Testing.UI/PageObjects/ProfilePage.cs @@ -0,0 +1,18 @@ +namespace OrchardCoreContrib.Testing.UI.PageObjects; + +/// +/// Represents the profile page. +/// +/// The . +public class ProfilePage(IPage page) : AdminPage(page) +{ + /// + 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/test/OrchardCoreContrib.Testing.UI.Tests/PageObjects/PageFactoryTests.cs b/test/OrchardCoreContrib.Testing.UI.Tests/PageObjects/PageFactoryTests.cs new file mode 100644 index 0000000..5ec756e --- /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(IPage page) : PageBase(page) + { + public override string Slug => "/my-page"; + } +} From d5c1b37949eee5b852526c41adc442ef1c9667d5 Mon Sep 17 00:00:00 2001 From: Hisham Bin Ateya Date: Mon, 13 May 2024 02:40:15 +0300 Subject: [PATCH 2/3] Initialize page factory in the UITest --- src/OrchardCoreContrib.Testing.UI/UITestOfT.cs | 8 ++++++++ 1 file changed, 8 insertions(+) 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); + } } From 92ac48a7190a64625390d70a48c33a46ef3fd635 Mon Sep 17 00:00:00 2001 From: Hisham Bin Ateya Date: Mon, 13 May 2024 02:55:26 +0300 Subject: [PATCH 3/3] Remove constructor from PageBase --- src/OrchardCoreContrib.Testing.UI/PageObjects/AdminPage.cs | 7 +++---- .../PageObjects/ChangePasswordPage.cs | 3 +-- .../PageObjects/DashboardPage.cs | 3 +-- src/OrchardCoreContrib.Testing.UI/PageObjects/LoginPage.cs | 3 +-- src/OrchardCoreContrib.Testing.UI/PageObjects/PageBase.cs | 5 ++--- .../PageObjects/PageFactory.cs | 3 ++- .../PageObjects/ProfilePage.cs | 3 +-- .../PageObjects/PageFactoryTests.cs | 2 +- 8 files changed, 12 insertions(+), 17 deletions(-) diff --git a/src/OrchardCoreContrib.Testing.UI/PageObjects/AdminPage.cs b/src/OrchardCoreContrib.Testing.UI/PageObjects/AdminPage.cs index 097844c..6dd45ee 100644 --- a/src/OrchardCoreContrib.Testing.UI/PageObjects/AdminPage.cs +++ b/src/OrchardCoreContrib.Testing.UI/PageObjects/AdminPage.cs @@ -6,8 +6,7 @@ namespace OrchardCoreContrib.Testing.UI.PageObjects; /// /// Represents a base class for admin page objects. /// -/// -public abstract class AdminPage(IPage page) : PageBase(page) +public abstract class AdminPage : PageBase { /// public abstract override string Slug { get; } @@ -37,7 +36,7 @@ public async Task ChangeThemeAsync(ThemeMode themeMode) /// 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(); + 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 index d73c761..f99301b 100644 --- a/src/OrchardCoreContrib.Testing.UI/PageObjects/ChangePasswordPage.cs +++ b/src/OrchardCoreContrib.Testing.UI/PageObjects/ChangePasswordPage.cs @@ -5,8 +5,7 @@ namespace OrchardCoreContrib.Testing.UI.PageObjects; /// /// Represents a change password page. /// -/// The . -public class ChangePasswordPage(IPage page) : AdminPage(page) +public class ChangePasswordPage : AdminPage { /// public override string Slug => "ChangePassword"; diff --git a/src/OrchardCoreContrib.Testing.UI/PageObjects/DashboardPage.cs b/src/OrchardCoreContrib.Testing.UI/PageObjects/DashboardPage.cs index 509a06f..45d1311 100644 --- a/src/OrchardCoreContrib.Testing.UI/PageObjects/DashboardPage.cs +++ b/src/OrchardCoreContrib.Testing.UI/PageObjects/DashboardPage.cs @@ -3,8 +3,7 @@ /// /// Represents the dashboard page. /// -/// The . -public class DashboardPage(IPage page) : AdminPage(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 index 4c31496..ea5d88d 100644 --- a/src/OrchardCoreContrib.Testing.UI/PageObjects/LoginPage.cs +++ b/src/OrchardCoreContrib.Testing.UI/PageObjects/LoginPage.cs @@ -6,8 +6,7 @@ namespace OrchardCoreContrib.Testing.UI.PageObjects; /// /// Represents a login page. /// -/// The . -public class LoginPage(IPage page) : PageBase(page) +public class LoginPage : PageBase { /// public override string Slug => "Login"; diff --git a/src/OrchardCoreContrib.Testing.UI/PageObjects/PageBase.cs b/src/OrchardCoreContrib.Testing.UI/PageObjects/PageBase.cs index b9ca1a4..fa0a4b0 100644 --- a/src/OrchardCoreContrib.Testing.UI/PageObjects/PageBase.cs +++ b/src/OrchardCoreContrib.Testing.UI/PageObjects/PageBase.cs @@ -3,13 +3,12 @@ /// /// Represents a base class for page objects. /// -/// -public abstract class PageBase(IPage page) +public abstract class PageBase { /// /// Gets the underlying object. /// - public IPage Page => page; + public IPage Page { get; internal set; } /// /// Gets the slug of the page. diff --git a/src/OrchardCoreContrib.Testing.UI/PageObjects/PageFactory.cs b/src/OrchardCoreContrib.Testing.UI/PageObjects/PageFactory.cs index 2e38b44..d46ecc9 100644 --- a/src/OrchardCoreContrib.Testing.UI/PageObjects/PageFactory.cs +++ b/src/OrchardCoreContrib.Testing.UI/PageObjects/PageFactory.cs @@ -27,8 +27,9 @@ public static async Task InitializeAsync(IBrowser browser, string baseUrl) /// The page type. public static async Task CreateAsync() where TPage : PageBase { - var page = (TPage)Activator.CreateInstance(typeof(TPage), _page); + 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 index 730ee57..46a55ff 100644 --- a/src/OrchardCoreContrib.Testing.UI/PageObjects/ProfilePage.cs +++ b/src/OrchardCoreContrib.Testing.UI/PageObjects/ProfilePage.cs @@ -3,8 +3,7 @@ /// /// Represents the profile page. /// -/// The . -public class ProfilePage(IPage page) : AdminPage(page) +public class ProfilePage : AdminPage { /// public override string Slug => "Admin/Users/Edit"; diff --git a/test/OrchardCoreContrib.Testing.UI.Tests/PageObjects/PageFactoryTests.cs b/test/OrchardCoreContrib.Testing.UI.Tests/PageObjects/PageFactoryTests.cs index 5ec756e..2313293 100644 --- a/test/OrchardCoreContrib.Testing.UI.Tests/PageObjects/PageFactoryTests.cs +++ b/test/OrchardCoreContrib.Testing.UI.Tests/PageObjects/PageFactoryTests.cs @@ -22,7 +22,7 @@ public async Task CreatePage() Assert.Equal("/my-page", page.Slug); } - public class MyPage(IPage page) : PageBase(page) + public class MyPage : PageBase { public override string Slug => "/my-page"; }