Lesson 3: Good and bad locators
Map one screen twice, with stable and with fragile locators, and see which of them survive a redesign.
Tutorials · Lesson 3 of 6 · about 25 minutes
git checkout lesson-2 in VAF-Tutorial.
A test finds every element by its locator. When a locator depends on something that changes — the layout, a text, a generated class name — the test breaks with the next release of the application, although nothing is wrong with it. In this lesson you map the same screen twice and watch a redesign break one of the two.
Look at the page
Open good and bad locators. It shows the same order summary twice. Inspect both with the browser's developer tools:
-
A. Built for testing — an
idon every single element (order-total,courier-note…), anddata-teston the rows of the item list, which repeat and cannot all have the sameid. -
B. Built without testing in mind — no ids, class names like
css-1x9f3kthat a styling library generates,divs insidedivs, and a note field whoseidis different every time you reload the page.
Now click Version 2 (after a redesign). Both sections look different: the title is
reworded, the items are in another order, the buttons have new texts. The ids and data-test
attributes of section A did not change.
Map section A
Add MappingPage.cs to PageObjects, the same way as the form in lesson 2:
FirstVafTest/PageObjects/MappingPage.cs
On GitHub
using OpenQA.Selenium;
using VisionAutomationFramework.Core;
namespace FirstVafTest.PageObjects
{
// https://www.visionautomationframework.com/practice/mapping, section A: built for testing.
// An id on every single element, data-test on the elements that repeat. Both stay the same
// after the redesign (?version=2).
public static class MappingPage
{
public class Order : ISectionContext
{
public By SectionFindMechanism { get; set; } = By.Id("order-stable");
public class Status : IElementContext
{
public By ElementFindMechanism { get; set; } = By.Id("order-status");
}
// The name of every item: the rows repeat, so data-test instead of an id
public class ItemName : IElementContext
{
public By ElementFindMechanism { get; set; } = By.CssSelector("[data-test='order-item'] [data-test='item-name']");
}
public class Total : IElementContext
{
public By ElementFindMechanism { get; set; } = By.Id("order-total");
}
public class Note : IElementContext
{
public By ElementFindMechanism { get; set; } = By.Id("courier-note");
}
public class SaveNote : IElementContext
{
public By ElementFindMechanism { get; set; } = By.Id("save-note");
}
public class Cancel : IElementContext
{
public By ElementFindMechanism { get; set; } = By.Id("cancel-order");
}
public class Message : IElementContext
{
public By ElementFindMechanism { get; set; } = By.Id("order-message");
}
}
}
}
ItemName matches the name in every row of the list, not one element. That is on
purpose: the test will read all of them at once.
Map section B — the way you should not
Section B gives you nothing stable, so map it the way it often happens: with whatever the developer
tools offer. Add MappingPageFragile.cs to PageObjects:
FirstVafTest/PageObjects/MappingPageFragile.cs
On GitHub
using OpenQA.Selenium;
using VisionAutomationFramework.Core;
namespace FirstVafTest.PageObjects
{
// https://www.visionautomationframework.com/practice/mapping, section B: built without testing in mind.
// DON'T MAP A PAGE LIKE THIS. Every locator below works today and breaks with the redesign
// (?version=2); each one says why. Section B has no id to start from, so the elements are not
// nested in a section: they are looked for in the whole page.
public static class MappingPageFragile
{
// By text: version 2 says "Your order 1042". Texts get reworded and translated.
public class Title : IElementContext
{
public By ElementFindMechanism { get; set; } = By.XPath("//span[text()='Order #1042']");
}
// By a generated class: the styling library makes new ones with every build.
public class Status : IElementContext
{
public By ElementFindMechanism { get; set; } = By.CssSelector(".css-1x9f3k");
}
// By position: version 2 lists the items in another order, so "the third row" is another item.
public class ThirdItemName : IElementContext
{
public By ElementFindMechanism { get; set; } = By.XPath("(//div[contains(@class,'d-flex gap-2 border-bottom')])[3]/div[1]/span");
}
// An absolute XPath, as the browser's developer tools copy it: anything that moves above it breaks it.
public class Total : IElementContext
{
public By ElementFindMechanism { get; set; } = By.XPath("/html/body/main/div/div[1]/div[2]/section/div/div/div[4]/div/span[2]");
}
}
}
Each locator works today, and each depends on something a redesign changes: a text, a generated class, a
position, the whole path from body down. Section B has no id to start from, so
these elements are not nested in a section; VAF looks for them in the whole page.
Test both, before and after the redesign
Add LocatorTests.cs to Tests:
FirstVafTest/Tests/LocatorTests.cs
On GitHub
using FirstVafTest.PageObjects;
using NUnit.Framework;
using VisionAutomationFramework;
using VisionAutomationFramework.Browsers;
using VisionAutomationFramework.Extensions;
namespace FirstVafTest.Tests
{
public class LocatorTests
{
private const string Page = "https://www.visionautomationframework.com/practice/mapping";
// The same test passes before and after the redesign: the ids and data-test did not change.
[TestCase(Page)]
[TestCase(Page + "?version=2")]
public void StableLocatorsSurviveTheRedesign(string url)
{
var browser = Testing.On<Chrome>();
try
{
var app = browser.LetsNavigateTo(url).Should().DisplayPage<AppPage>().That;
app.Should().HaveSection<MappingPage.Order>()
.That.Should().HaveElement<MappingPage.Order.Total>()
.That.AsLabelText().Should().HaveValue("€39.50")
.AndAlso().Should().HaveElement<MappingPage.Order.Note>()
.That.AsTextInputField().LetsInsert("Ring twice")
.AndAlso().Should().HaveElement<MappingPage.Order.SaveNote>()
.That.AsButton().LetsClick()
.AndAlso().Should().HaveElement<MappingPage.Order.Message>()
.That.AsLabelText().Should().HaveValue("Note saved: Ring twice")
.AndAlso().Should().HaveElement<MappingPage.Order.Cancel>()
.That.AsButton().LetsClick()
.AndAlso().Should().HaveElement<MappingPage.Order.Status>()
.That.AsLabelText().Should().HaveValue("Cancelled");
// Version 2 lists the items in another order, so check them without an order
var items = app.Should().HaveSection<MappingPage.Order>()
.That.LetsGetTextValuesOfElements<MappingPage.Order.ItemName>();
Assert.That(items, Is.EquivalentTo(new[] { "Coffee beans, 1 kg", "Ceramic mug", "Paper filters" }));
}
finally
{
browser.Scope.Driver.Quit();
}
}
[Test]
public void FragileLocatorsWorkOnVersion1()
{
var browser = Testing.On<Chrome>();
try
{
var app = browser.LetsNavigateTo(Page).Should().DisplayPage<AppPage>().That;
app.Should().HaveElement<MappingPageFragile.Title>()
.That.AsLabelText().Should().HaveValue("Order #1042");
app.Should().HaveElement<MappingPageFragile.Status>()
.That.AsLabelText().Should().HaveValue("Paid");
app.Should().HaveElement<MappingPageFragile.ThirdItemName>()
.That.AsLabelText().Should().HaveValue("Paper filters");
app.Should().HaveElement<MappingPageFragile.Total>()
.That.AsLabelText().Should().HaveValue("€39.50");
}
finally
{
browser.Scope.Driver.Quit();
}
}
// The same locators after the redesign. The page still shows all four values.
[Test]
public void FragileLocatorsBreakOnVersion2()
{
var browser = Testing.On<Chrome>();
try
{
var app = browser.LetsNavigateTo(Page + "?version=2").Should().DisplayPage<AppPage>().That;
// Three locators find nothing any more. BePresent answers true or false instead of failing.
Assert.That(app.Should().BePresent<MappingPageFragile.Title>(seconds: 2), Is.False);
Assert.That(app.Should().BePresent<MappingPageFragile.Status>(seconds: 2), Is.False);
Assert.That(app.Should().BePresent<MappingPageFragile.Total>(seconds: 2), Is.False);
// The worst one: the third row is still there, so the locator works and reads another item
string third = app.Should().HaveElement<MappingPageFragile.ThirdItemName>()
.That.AsLabelText().LetsGetValue();
Assert.That(third, Is.EqualTo("Coffee beans, 1 kg"));
}
finally
{
browser.Scope.Driver.Quit();
}
}
}
}
-
StableLocatorsSurviveTheRedesignruns twice, once per[TestCase]: the same test, against version 1 and version 2. It saves a note, cancels the order and reads all item names withLetsGetTextValuesOfElements. The items are compared withIs.EquivalentTo, which ignores their order — the redesign changed it, and the order is not what this test is about. -
FragileLocatorsWorkOnVersion1shows that the fragile page objects are not wrong today. -
FragileLocatorsBreakOnVersion2shows what the redesign did to them. Three find nothing any more;BePresentanswersfalseinstead of failing the test. The fourth is worse: there is still a third row, so the locator works and silently reads another item.
Run it, then see a redesign fail a test
Run all tests: six pass, two of them from the earlier lessons. Then do what a release of the application
does to a real suite: in FragileLocatorsWorkOnVersion1, change Page to
Page + "?version=2" and run it again.
It waits 10 seconds for the title and fails because the element is not present. Nothing is wrong with
the page — it shows the title, only with other words. With fragile locators, every redesign looks like a
bug until someone checks, and then every page object needs fixing. Put Page back.
Every test in LocatorTests also repeats opening and closing the browser.
Lesson 5 moves that into one place.
Choose locators that last
From the most to the least stable:
idanddata-test— made for finding the element; a redesign leaves them alone.name, a label, ARIA attributes (role,aria-label) — part of the form and of accessibility, so they change rarely.- CSS classes — made for the look; a new design changes them.
- Texts — get reworded and translated, and are rarely unique: both sections have a Cancel button, and a locator by its text finds the first one.
- Positions and absolute XPath — break when anything above or around the element moves.
- Generated ids and classes — change with every build, or every page load.
When a page offers only the bottom of this list, the fix is not a cleverer XPath: ask its developers for an
id or a data-test attribute. It is one line of their code, and it saves a
broken test with every release.
Compare with the reference code
The project as it is at the end of this lesson is tag
lesson-3 of
VAF-Tutorial. If your test does not pass,
compare your files with it, or get it with git checkout lesson-3.