Lesson 4: More controls
Drop-down lists, check boxes, radio buttons, a date picker and a file upload, and the checks that go with them.
Tutorials · Lesson 4 of 6 · about 30 minutes
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.
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.
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.
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 —
HaveOptionschecks every option, in order;LetsSelectchooses one by its text, andBeSelectedchecks which one is chosen. Checks can follow each other:.HaveOptions(…).BeSelected(…). -
Radio buttons —
BeSelectedandBeUnSelected. 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 —
AsCounterFieldfor the number of errors,AsLabelTextfor each message.
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.
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");
}
}
}
-
LetsSelectDatetakes aDateTime: 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. -
LetsUploadneeds 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.
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.