Skip to main content

Model a screen as a page object

The problem​

A browser test full of CSS selectors says nothing about which screen broke. When the markup changes, every test that copied the selector changes with it.

A page object describes a screen, so a test names elements instead of selectors.

Do it​

1. Describe the screens​

Pages.cs describes the two screens the browser journey uses. Each property names one element:

Pages.cs4 notes
1public sealed class SignInPage : WebPage
2{
3public WebElement Token => Element(By.TestId("token"));
4
5public WebElement Submit => Element(By.TestId("login"));
6
7public WebElement Error => Element(By.TestId("error"));
8}
9
10public sealed class ProjectsPage : WebPage
11{
12public WebElement Table => Element(By.TestId("projects"));
13
14public WebElement Search => Element(By.TestId("search"));
15
16public WebComponentCollection<ProjectRow> Rows => Components<ProjectRow>(By.TestId("project"));
17
18public ProjectRow Project(string name) => Rows.Matching(By.HasText(name), $"Project[{name}]");
19}
20
21public sealed class ProjectRow : WebComponent
22{
23public WebElement Name => Element(By.TestId("project-name"));
24
25public WebElement Status => Element(By.TestId("project-status"));
26}
  1. One property, one element

    Found on the live page each time it is read.

  2. Rows are components

    Each row has its own class with its own elements.

  3. Matching is strict

    It finds exactly one row, or fails.

  4. Found inside its row

    Status is looked up within the matched row, not the whole page.

From samples/Northstar.ProtoTest/Pages.cs. The wait the journeys use lives beside it in NorthstarPages.Wait.

2. Write the journey as a user​

var signIn = Proto.Context.Web().Page<SignInPage>();
await signIn.OpenAsync("/login");
await signIn.Flow("Sign in with the tenant token")
.Fill(page => page.Token, organization.OwnerToken)
.Click(page => page.Submit)
.RunAsync();

var projects = Proto.Context.Web().Page<ProjectsPage>();
var row = projects.Project(name);
await row.Status.Should.HaveTextAsync(ProjectStatuses.Active, NorthstarPages.Wait);

3. Run it and read the trace​

dotnet test samples/Northstar.ProtoTest --filter "FullyQualifiedName~WebJourney"

The execution phase shows web.navigate, web.flow, web.fill, web.click and assert.web. Each entry carries the component path, from ProjectsPage down to the element the step touched.

If the journey skips, the reason names the missing browser. The web integration page covers the install.

4. Break one locator and read the failure​

In Pages.cs, change the row's status test id from project-status to project-state, then run the journey again. It fails after the 15-second wait:

Element 'ProjectsPage.Project[browser-192244000001].Status' should have text "active" within 00:00:15.
Last observed: Element 'ProjectsPage.Project[browser-192244000001].Status' was not present within 00:00:05.
Locator: TestId("project-state").

The message reads like the test: the projects page, the row for this test's project, its status. The locator comes last, as the detail to fix. Each probe waits up to the 5-second action timeout for the element, so the last observation names that shorter wait.

Change the test id back before you go on.

What happened​

The page object holds how to find an element, in one named place. When the markup changes, one property changes, and the failure already said which one: the screen, the component and the element, with the locator as the last detail.

Check yourself​

A step in the browser journey fails on the projects screen. What three things does the trace name, and what would a raw selector name instead?

Verify
Run the WebJourney filter above and read the web.* entries.

Remember​

  • A page object names elements as properties. Each read finds the element on the live page.
  • The trace carries the component path of what each step touched.

Next: write an integration.

Go deeper​