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";
+ }
+}