To support .NET Developers and Testers in building Web application tests with composable PageObjects;
-
Removing PageObject and Test references to external query libraries with access to them behind an abstraction. (e.g WebDriver, AngleSharp... )
-
Provide common GDS components with default mappings
-
Encourage composition of PageObjects so that different types of tests can use the same PageObjects.
-
Ensure your test project is using dependency injection - DI
-
Choose a query library for your test project
-
Register your pages and components in your tests DI using
IPageObject
-
Use library provided Components in your pages with
ComponentFactory<TComponent>
-
Access pages in your tests with
IDocumentSessionClient
// TODO document configuration options for each provider; WebDriverSession, AngleSharpParseOptions (AlwaysUseContext, TextReplacement) - needs to be decoratable for retrypolicy
services.AddAngleSharp();
services.AddWebApplicationFactory<TApplicationProgram>(); // TApplicationProgram is your Program class from your .NET Web Application
services.AddWebDriver();
// configure default WebDriverSessionOptions from configuration
services.ConfigureWebDriverSession(t => {
t.BrowserType = configuration.BrowserType
t.Headless = configuration.Headless;
t.BrowserOptions = configuration.BrowserOptions
});
// OR bind options against the WebDriverSessionOptions object
services.Configure<WebDriverSessionOptions>(configuration);
services.AddSingleton<WebDriverSessionOptions>(sp => sp.GetRequireService<IOptions<WebDriverSessionOptions>>.Value); // ensure non-options is registered
ComponentFactory<IComponent>
Role: create components with this
IComponent
Role: marks components available
// example of using library
public sealed class MyPage : IPageObject
{
ComponentFactory<HeaderComponent> _headerFactory
// constructor
public MyPage(ComponentFactory<HeaderComponent> headerFactory)
{
_headerFactory = headerFactory;
}
// simplest usage exposing the library type to tests
public HeaderComponent => _headerFactory.Get(... options)
}
The library supports below components (GDS and supporting e.g AnchorLink <a>
)
GDS Header
GDS Footer
GDS Button
GDS Checkboxes
GDS Radio
GDS Fieldset
GDS CookieBanner
GDS TextInput
GDS DateInput
- GDS
- Library use Fieldset
GDS Tabs
GDS Details
GDS ErrorMessage
GDS ErrorSummary
GDS Pagination
- GDS
- Library use AnchorLink
GDS Panel
GDS Select
GDS Table
---END GDS---
TODO ensure all supporting components are referenced here and reside outside of the GDSComponents folder
Anchor Link
Form
Label
Option
Table parts
When building your PageObjects you want to:
IPageObject
- Role: mark pages with this
IDocumentSessionClient
- Role: create pages with this
public sealed class HomePage : IPageObject
Important register your pages and application components in your DI!
testServices...
// top level PageObject
testServices.AddTransient<IPageObject, HomePage>();
testServices.AddTransient<NavigationBarComponent>(); // reusable application component
public sealed class HomePage
{
public HomePage(
NavigationBarComponent navBar,
SearchComponent search,
FilterComponent filter)
{
Search = search ?? throw new ArgumentNullException(nameof(search));
Filter = filter ?? throw new ArgumentNullException(nameof(filter));
NavBar = navBar ?? throw new ArgumentNullException(nameof(navBar));
}
// Reuse these across Pages
public NavigationBarComponent NavBar { get; }
public SearchComponent Search { get; }
public FilterComponent Filter { get; }
}
Note Ensure you have Registered your pages and application components
public sealed class MyTestClass : BaseTest
{
[Fact]
public async Task MyTest()
{
// create document
IDocumentSessionClient documentSessionClient = await GetTestService<IDocumentSessionClient>();
// pageobjects are not coupled to the path request made for a document
await documentSession.RequestDocumentAsync(
(t) => t.SetPath("/"));
// create page from the documentSession
HomePage page = documentSession.GetPage<HomePage>();
}
}
Note You could aggregate a/multiple components to your own tests type e.g Facets
or SearchResults
being made up of multiple GDS components to make your PageObject API more domain centric
// Basic types example
homePage.GetHeading().Should().Be("Heading");
public sealed class HomePage
{
public string GetHeading() => _headingFactory.GetHeadings().Where(t => t.Type == H1).Text;
}
// Library types example
// GDSComponent provided by the library - record to give value-object semantics (immutable, attribute comparisons, no identity)
public record GDSTextInput
{
public required string Name { get; init; }
public required string Value { get; init; }
public required string? PlaceHolder { get; init; } = null;
public required string? Type { get; init; } = null;
}
// Page
public sealed class HomePage
{
public HomePage(ComponentFactory<GDSTextInput> inputFactory)
public GDSTextInput GetSearchInput() => inputFactory.Get(...);
}
// Test
[Fact]
public async Test()
{
GDSTextInput expectedTextInput = new()
{
Name = "searchKeyWord",
Value = "",
PlaceHolder = "Search by keyword",
Type = "text"
};
homePage.GetSearchInput().Should().Be(expectedTextInput);
}
// This uses the Singleton pattern that wraps the DependencyInjection container ensuring a single instance of the container and, for services to be registered via `IServiceCollection`
internal sealed class DependencyInjection
{
private static readonly DependencyInjection _instance = new();
private readonly IServiceProvider _serviceProvider;
static DependencyInjection()
{
}
private DependencyInjection()
{
IServiceCollection services = new ServiceCollection()
// ToAddAngleSharp .AddAngleSharp<Program>();
// ToAddWebDriver .AddWebDriver();
//
services.AddTransient<IPageObject, ApplicationHomePage>();
services.AddTransient<ApplicationComponent>();
_serviceProvider = services.BuildServiceProvider();
}
public static DependencyInjection Instance
{
get
{
return _instance;
}
}
internal IServiceScope CreateScope() => _serviceProvider.CreateScope();
}
// Separately you may want to make this Scope started and disposed in a base test class.
public abstract class BaseTest : IDisposable
{
private readonly IServiceScope _serviceScope;
protected BaseHttpTest()
{
_serviceScope = DependencyInjection.Instance.CreateScope();
}
protected T GetTestService<T>()
=> _serviceScope.ServiceProvider.GetService<T>()
?? throw new ArgumentNullException($"Unable to resolve type {typeof(T)}");
public void Dispose()
{
GC.SuppressFinalize(this);
_serviceScope.Dispose();
}
}
// Any test class you inherit from BaseTest gets access to the container and a new scope is created per test
public sealed class MyTestClass : BaseTest
{
[Fact]
public async Task MyTest()
{
IDocumentSession documentSession = GetTestService<IDocumentSession>(); // is available
}
}