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:
1public sealed class SignInPage : WebPage2{3public WebElement Token => Element(By.TestId("token"));45public WebElement Submit => Element(By.TestId("login"));67public WebElement Error => Element(By.TestId("error"));8}910public sealed class ProjectsPage : WebPage11{12public WebElement Table => Element(By.TestId("projects"));1314public WebElement Search => Element(By.TestId("search"));1516public WebComponentCollection<ProjectRow> Rows => Components<ProjectRow>(By.TestId("project"));1718public ProjectRow Project(string name) => Rows.Matching(By.HasText(name), $"Project[{name}]");19}2021public sealed class ProjectRow : WebComponent22{23public WebElement Name => Element(By.TestId("project-name"));2425public WebElement Status => Element(By.TestId("project-status"));26}
- One property, one element
Found on the live page each time it is read.
- Rows are components
Each row has its own class with its own elements.
- Matching is strict
It finds exactly one row, or fails.
- Found inside its row
Status is looked up within the matched row, not the whole page.
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?
web.* entries.The screen, the component and the element, as one path from ProjectsPage down. A raw selector names only the CSS.
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
- Web integration: install, probe and the page object model.