Tutorials · Lesson 4 of 6 · about 30 minutes

Start from lesson 3. You need the project from Good and bad locators, with its tests passing, or git checkout lesson-3 in VAF-Tutorial.

After HaveElement<…>() you tell VAF what kind of control the element is — AsTextInputField(), AsSelectBox(), AsCheckBox()… — and that decides the actions and checks you get next. This lesson goes through the controls of a typical form, plus a date picker and a file upload. The guide has the full table.

1

Map the rest of the form

In lesson 2 you mapped only what one test needed. Add the rest of the sign-up form to FormsPage.cs: the three plans, the error messages the server shows next to the fields, and the plan in the result. The whole file now looks like this:

FirstVafTest/PageObjects/FormsPage.cs On GitHub
using OpenQA.Selenium;
using VisionAutomationFramework.Core;

namespace FirstVafTest.PageObjects
{
    // https://www.visionautomationframework.com/practice/forms
    // A static class per screen only groups the sections of that screen.
    public static class FormsPage
    {
        // The sign-up form
        public class Registration : ISectionContext
        {
            public By SectionFindMechanism { get; set; } = By.Id("registration");

            public class FullName : IElementContext
            {
                public By ElementFindMechanism { get; set; } = By.Id("full-name");
            }

            public class Email : IElementContext
            {
                public By ElementFindMechanism { get; set; } = By.Id("email");
            }

            public class Password : IElementContext
            {
                public By ElementFindMechanism { get; set; } = By.Id("password");
            }

            public class Country : IElementContext
            {
                public By ElementFindMechanism { get; set; } = By.Id("country");
            }

            // Each plan is a radio button of its own
            public class PlanFree : IElementContext
            {
                public By ElementFindMechanism { get; set; } = By.Id("plan-free");
            }

            public class PlanTeam : IElementContext
            {
                public By ElementFindMechanism { get; set; } = By.Id("plan-team");
            }

            public class PlanEnterprise : IElementContext
            {
                public By ElementFindMechanism { get; set; } = By.Id("plan-enterprise");
            }

            public class AcceptTerms : IElementContext
            {
                public By ElementFindMechanism { get; set; } = By.Id("accept-terms");
            }

            public class Submit : IElementContext
            {
                public By ElementFindMechanism { get; set; } = By.Id("submit-button");
            }

            // Shown by the server after a form with errors is sent
            public class ErrorCount : IElementContext
            {
                public By ElementFindMechanism { get; set; } = By.Id("error-count");
            }

            public class FullNameError : IElementContext
            {
                public By ElementFindMechanism { get; set; } = By.Id("full-name-error");
            }

            public class EmailError : IElementContext
            {
                public By ElementFindMechanism { get; set; } = By.Id("email-error");
            }

            public class PasswordError : IElementContext
            {
                public By ElementFindMechanism { get; set; } = By.Id("password-error");
            }

            public class CountryError : IElementContext
            {
                public By ElementFindMechanism { get; set; } = By.Id("country-error");
            }
        }

        // What the server received, shown after a valid form is sent
        public class Result : ISectionContext
        {
            public By SectionFindMechanism { get; set; } = By.Id("result");

            public class Title : IElementContext
            {
                public By ElementFindMechanism { get; set; } = By.Id("result-title");
            }

            public class FullName : IElementContext
            {
                public By ElementFindMechanism { get; set; } = By.Id("result-full-name");
            }

            public class Email : IElementContext
            {
                public By ElementFindMechanism { get; set; } = By.Id("result-email");
            }

            public class Country : IElementContext
            {
                public By ElementFindMechanism { get; set; } = By.Id("result-country");
            }

            public class Plan : IElementContext
            {
                public By ElementFindMechanism { get; set; } = By.Id("result-plan");
            }
        }
    }
}

Each plan is a radio button with its own id, so each gets its own class. The test from lesson 2 keeps working: you only added elements.

2

Open the page once per test

All tests of this lesson start on the same page. Instead of opening and closing the browser in each one, let NUnit do it: a method marked [SetUp] runs before every test of the class, and one marked [TearDown] after it — also when the test fails, so no browser is left open. The page context goes into a field, app, which every test uses. You will see it at the top of the next file.

= null! tells the compiler the fields are set before they are used (in SetUp); without it, the project's nullable setting warns about them.

3

Drop-down list, radio buttons, check box

Add FormControlsTests.cs to Tests:

FirstVafTest/Tests/FormControlsTests.cs On GitHub
using System.Collections.Generic;
using FirstVafTest.PageObjects;
using NUnit.Framework;
using VisionAutomationFramework;
using VisionAutomationFramework.Actions;
using VisionAutomationFramework.Browsers;
using VisionAutomationFramework.Extensions;

namespace FirstVafTest.Tests
{
    public class FormControlsTests
    {
        private BrowserActions browser = null!;     // set in SetUp, before every test
        private PageActions<AppPage> app = null!;

        // Every test starts on the same page, so open it once here instead of in every test
        [SetUp]
        public void OpenTheForm()
        {
            browser = Testing.On<Chrome>();
            app = browser.LetsNavigateTo("https://www.visionautomationframework.com/practice/forms")
                .Should().DisplayPage<AppPage>().That;
        }

        // Runs after every test, also when it fails
        [TearDown]
        public void CloseTheBrowser() => browser.Scope.Driver.Quit();

        [Test]
        public void CountryListOffersEveryCountry()
        {
            var countries = new List<string>
            {
                "Choose a country", "Bosnia and Herzegovina", "Croatia", "Germany", "Montenegro", "Serbia",
                "United Kingdom", "United States"
            };

            app.Should().HaveSection<FormsPage.Registration>()
                .That.Should().HaveElement<FormsPage.Registration.Country>()
                    .That.AsSelectBox().Should().HaveOptions(countries)
                        .BeSelected("Choose a country")
                .AndAlso().Should().HaveElement<FormsPage.Registration.Country>()
                    .That.AsSelectBox().LetsSelect("Montenegro")
                .AndAlso().Should().HaveElement<FormsPage.Registration.Country>()
                    .That.AsSelectBox().Should().BeSelected("Montenegro");
        }

        [Test]
        public void OnlyOnePlanIsSelected()
        {
            app.Should().HaveSection<FormsPage.Registration>()
                .That.Should().HaveElement<FormsPage.Registration.PlanFree>()
                    .That.AsRadioButton().Should().BeSelected()
                .AndAlso().Should().HaveElement<FormsPage.Registration.PlanTeam>()
                    .That.AsRadioButton().LetsClick()
                .AndAlso().Should().HaveElement<FormsPage.Registration.PlanTeam>()
                    .That.AsRadioButton().Should().BeSelected()
                .AndAlso().Should().HaveElement<FormsPage.Registration.PlanFree>()
                    .That.AsRadioButton().Should().BeUnSelected();
        }

        [Test]
        public void SubmitIsEnabledOnceTheTermsAreAccepted()
        {
            app.Should().HaveSection<FormsPage.Registration>()
                .That.Should().HaveElement<FormsPage.Registration.Submit>()
                    .That.AsButton().Should().BeDisabled()
                .AndAlso().Should().HaveElement<FormsPage.Registration.AcceptTerms>()
                    .That.AsCheckBox().Should().BeUnChecked()
                .AndAlso().Should().HaveElement<FormsPage.Registration.AcceptTerms>()
                    .That.AsCheckBox().LetsClick()
                .AndAlso().Should().HaveElement<FormsPage.Registration.AcceptTerms>()
                    .That.AsCheckBox().Should().BeChecked()
                .AndAlso().Should().HaveElement<FormsPage.Registration.Submit>()
                    .That.AsButton().Should().BeEnabled();
        }

        // A check you have never seen fail may not check anything. This one must fail.
        [Test]
        public void CheckFailsWhenTheButtonIsDisabled()
        {
            Assert.Throws<VafAssertionException>(() =>
                app.Should().HaveSection<FormsPage.Registration>()
                    .That.Should().HaveElement<FormsPage.Registration.Submit>()
                        .That.AsButton().Should().BeEnabled());
        }

        [Test]
        public void EmptyFormShowsAnErrorPerField()
        {
            app.Should().HaveSection<FormsPage.Registration>()
                .That.Should().HaveElement<FormsPage.Registration.AcceptTerms>()
                    .That.AsCheckBox().LetsClick()
                .AndAlso().Should().HaveElement<FormsPage.Registration.Submit>()
                    .That.AsButton().LetsClick();

            app.Should().HaveSection<FormsPage.Registration>()
                .That.Should().HaveElement<FormsPage.Registration.ErrorCount>()
                    .That.AsCounterField().Should().HaveValue("4")
                .AndAlso().Should().HaveElement<FormsPage.Registration.FullNameError>()
                    .That.AsLabelText().Should().HaveValue("Please enter your full name.")
                .AndAlso().Should().HaveElement<FormsPage.Registration.EmailError>()
                    .That.AsLabelText().Should().HaveValue("Please enter your email address.")
                .AndAlso().Should().HaveElement<FormsPage.Registration.PasswordError>()
                    .That.AsLabelText().Should().HaveValue("Please choose a password.")
                .AndAlso().Should().HaveElement<FormsPage.Registration.CountryError>()
                    .That.AsLabelText().Should().HaveValue("Please choose a country.");
        }
    }
}
  • Drop-down list — HaveOptions checks every option, in order; LetsSelect chooses one by its text, and BeSelected checks which one is chosen. Checks can follow each other: .HaveOptions(…).BeSelected(…).
  • Radio buttons — BeSelected and BeUnSelected. Clicking Team unselects Free; the test checks both.
  • Check box and button — BeChecked, BeUnChecked, BeEnabled, BeDisabled. The form keeps its button disabled until the terms are accepted.
  • Messages from the server — AsCounterField for the number of errors, AsLabelText for each message.
4

A check that must fail

A check you have never seen fail may not check anything — a wrong locator or a wrong control type can make it pass whatever the page shows. CheckFailsWhenTheButtonIsDisabled proves that BeEnabled really looks at the button: before the terms are accepted it must fail, and Assert.Throws<VafAssertionException> passes only if it does.

Try it: at the start of that test, click AcceptTerms first. Now the button is enabled, the check passes, and the test fails, because the exception it expected never came.

5

Date picker and file upload

Two more pages: a date picker (flatpickr) and a file upload. Add their page objects to PageObjects:

FirstVafTest/PageObjects/DatePickersPage.cs On GitHub
using OpenQA.Selenium;
using VisionAutomationFramework.Core;

namespace FirstVafTest.PageObjects
{
    // https://www.visionautomationframework.com/practice/date-pickers
    public static class DatePickersPage
    {
        public class Single : ISectionContext
        {
            public By SectionFindMechanism { get; set; } = By.Id("single-demo");

            // Not the input: VAF works with the wrapper flatpickr puts around it
            public class Birthday : IElementContext
            {
                public By ElementFindMechanism { get; set; } = By.CssSelector("#birthday-field .flatpickr-wrapper");
            }

            public class Selected : IElementContext
            {
                public By ElementFindMechanism { get; set; } = By.Id("birthday-value");
            }
        }
    }
}
FirstVafTest/PageObjects/UploadPage.cs On GitHub
using OpenQA.Selenium;
using VisionAutomationFramework.Core;

namespace FirstVafTest.PageObjects
{
    // https://www.visionautomationframework.com/practice/upload
    // The server shows the name and size of what it received, and keeps nothing.
    public static class UploadPage
    {
        public class Form : ISectionContext
        {
            public By SectionFindMechanism { get; set; } = By.Id("upload-form");

            public class Single : IElementContext
            {
                public By ElementFindMechanism { get; set; } = By.Id("single-file");
            }

            public class Upload : IElementContext
            {
                public By ElementFindMechanism { get; set; } = By.Id("upload-button");
            }
        }

        public class Result : ISectionContext
        {
            public By SectionFindMechanism { get; set; } = By.Id("upload-result");

            public class Count : IElementContext
            {
                public By ElementFindMechanism { get; set; } = By.Id("received-count");
            }

            public class First : IElementContext
            {
                public By ElementFindMechanism { get; set; } = By.Id("received-1");
            }
        }
    }
}

and the tests to Tests:

FirstVafTest/Tests/DateAndUploadTests.cs On GitHub
using System;
using System.IO;
using FirstVafTest.PageObjects;
using NUnit.Framework;
using VisionAutomationFramework;
using VisionAutomationFramework.Actions;
using VisionAutomationFramework.Browsers;
using VisionAutomationFramework.Extensions;

namespace FirstVafTest.Tests
{
    public class DateAndUploadTests
    {
        private const string Site = "https://www.visionautomationframework.com";

        private BrowserActions browser = null!;     // set in SetUp, before every test

        [SetUp]
        public void StartTheBrowser() => browser = Testing.On<Chrome>();

        [TearDown]
        public void CloseTheBrowser() => browser.Scope.Driver.Quit();

        [Test]
        public void DateIsChosenInTheCalendar()
        {
            var date = new DateTime(2026, 10, 5);

            var app = browser.LetsNavigateTo(Site + "/practice/date-pickers")
                .Should().DisplayPage<AppPage>().That;

            // VAF opens the calendar, moves to the month and clicks the day
            app.Should().HaveSection<DatePickersPage.Single>()
                .That.Should().HaveElement<DatePickersPage.Single.Birthday>()
                    .That.AsDateTimeFlatpickerV4().LetsSelectDate(date)
                .AndAlso().Should().HaveElement<DatePickersPage.Single.Birthday>()
                    .That.AsDateTimeFlatpickerV4().Should().HaveValue(date, "yyyy-MM-dd")
                .AndAlso().Should().HaveElement<DatePickersPage.Single.Selected>()
                    .That.AsLabelText().Should().HaveValue("2026-10-05");
        }

        [Test]
        public void FileIsUploaded()
        {
            // A small file with a known size, in the test's own folder
            string file = Path.Combine(TestContext.CurrentContext.WorkDirectory, "hello.txt");
            File.WriteAllText(file, "Hello VAF");

            var app = browser.LetsNavigateTo(Site + "/practice/upload")
                .Should().DisplayPage<AppPage>().That;

            app.Should().HaveSection<UploadPage.Form>()
                .That.Should().HaveElement<UploadPage.Form.Single>()
                    .That.AsUploadFileField().LetsUpload(file)
                .AndAlso().Should().HaveElement<UploadPage.Form.Upload>()
                    .That.AsButton().LetsClick();

            app.Should().HaveSection<UploadPage.Result>()
                .That.Should().HaveElement<UploadPage.Result.Count>()
                    .That.AsCounterField().Should().HaveValue("1")
                .AndAlso().Should().HaveElement<UploadPage.Result.First>()
                    .That.AsLabelText().Should().HaveValue("hello.txt — 9 bytes");
        }
    }
}
  • LetsSelectDate takes a DateTime: VAF opens the calendar, moves to the right month and year and clicks the day, like a user. HaveValue(date, "yyyy-MM-dd") compares the field with the date in the format the page uses.
  • LetsUpload needs the full path of a file on the machine that runs the test. The test writes a small file into its own output folder first, so it works on every machine and in a build pipeline.
6

Run it

Run all tests: thirteen pass. Your test classes now repeat themselves in another way — every one of them has its own SetUp and TearDown, and the same chains of steps appear in several tests. Lesson 5 moves both into one place.

Compare with the reference code

The project as it is at the end of this lesson is tag lesson-4 of VAF-Tutorial. If your test does not pass, compare your files with it, or get it with git checkout lesson-4.