Tutorials · Lesson 3 of 6 · about 25 minutes

Start from lesson 2. You need the project from Map your first page, with its tests passing, or 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.

1

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 id on every single element (order-total, courier-note…), and data-test on the rows of the item list, which repeat and cannot all have the same id.
  • B. Built without testing in mind — no ids, class names like css-1x9f3k that a styling library generates, divs inside divs, and a note field whose id is 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.

2

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.

3

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.

4

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();
            }
        }
    }
}
  • StableLocatorsSurviveTheRedesign runs 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 with LetsGetTextValuesOfElements. The items are compared with Is.EquivalentTo, which ignores their order — the redesign changed it, and the order is not what this test is about.
  • FragileLocatorsWorkOnVersion1 shows that the fragile page objects are not wrong today.
  • FragileLocatorsBreakOnVersion2 shows what the redesign did to them. Three find nothing any more; BePresent answers false instead of failing the test. The fourth is worse: there is still a third row, so the locator works and silently reads another item.
5

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.

6

Choose locators that last

From the most to the least stable:

  1. id and data-test — made for finding the element; a redesign leaves them alone.
  2. name, a label, ARIA attributes (role, aria-label) — part of the form and of accessibility, so they change rarely.
  3. CSS classes — made for the look; a new design changes them.
  4. 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.
  5. Positions and absolute XPath — break when anything above or around the element moves.
  6. 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.