Tutorials · Lesson 6 of 6 · about 25 minutes

Start from lesson 5. You need the project from Shared steps and a test base, with its tests passing, or git checkout lesson-5 in VAF-Tutorial.

Real applications are slow in places: a list loads behind a spinner, a message appears a moment after a click. A test that checks too early fails although nothing is wrong. This lesson shows how VAF waits — without a single sleep in your tests — and how a failed test tells you what happened.

1

Look at the page

Open loading and waiting. Click Load results: a spinner turns for one and a half seconds, then three results appear and the counter goes from 0 to 1. Click Show the message: the message appears two seconds later.

2

Map it

Add LoadingPage.cs to PageObjects:

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

namespace FirstVafTest.PageObjects
{
    // https://www.visionautomationframework.com/practice/loading
    public static class LoadingPage
    {
        // Shown while the results load. Not in a section: VAF looks for the loader in the whole page.
        public class Spinner : IElementContext
        {
            public By ElementFindMechanism { get; set; } = By.Id("loading-spinner");
        }

        // Results that come from the server one and a half seconds after the click
        public class SpinnerDemo : ISectionContext
        {
            public By SectionFindMechanism { get; set; } = By.Id("spinner-demo");

            public class Load : IElementContext
            {
                public By ElementFindMechanism { get; set; } = By.Id("load-results");
            }

            // How many times the results were loaded: "0" until the first load is done
            public class LoadCount : IElementContext
            {
                public By ElementFindMechanism { get; set; } = By.Id("load-count");
            }

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

        // A message that appears two seconds after the click
        public class LateDemo : ISectionContext
        {
            public By SectionFindMechanism { get; set; } = By.Id("late-demo");

            public class Show : IElementContext
            {
                public By ElementFindMechanism { get; set; } = By.Id("show-later");
            }

            public class Message : IElementContext
            {
                public By ElementFindMechanism { get; set; } = By.Id("late-message");
            }
        }
    }
}

The spinner is an element of its own, outside any section: you will hand it to VAF as the loader, and VAF looks for a loader in the whole page.

3

Two kinds of waiting

Add WaitingTests.cs to Tests:

FirstVafTest/Tests/WaitingTests.cs On GitHub
using FirstVafTest.Common;
using FirstVafTest.PageObjects;
using NUnit.Framework;
using VisionAutomationFramework;
using VisionAutomationFramework.Extensions;

namespace FirstVafTest.Tests
{
    // No sleeps in these tests: VAF waits for elements and for the loader by itself
    public class WaitingTests : TestBase
    {
        [SetUp]
        public void Open() => SharedContext.LetsOpen("/practice/loading");

        [Test]
        public void ResultsAreReadAfterTheSpinnerIsGone()
        {
            // From now on, VAF waits for the spinner to disappear before every element it looks for
            SharedContext.Browser.LetsUseLoader<LoadingPage.Spinner>();

            SharedContext.App.Should().HaveSection<LoadingPage.SpinnerDemo>()
                .That.Should().HaveElement<LoadingPage.SpinnerDemo.Load>()
                    .That.AsButton().LetsClick()
                .AndAlso().Should().HaveElement<LoadingPage.SpinnerDemo.LoadCount>()
                    .That.AsCounterField().Should().HaveValue("1")
                .AndAlso().Should().HaveElement<LoadingPage.SpinnerDemo.FirstResult>()
                    .That.AsLabelText().Should().HaveValue("Alpha (load 1)");
        }

        // The same check without the loader comes too early: the counter still shows "0"
        [Test]
        public void WithoutTheLoaderTheCheckIsTooEarly()
        {
            SharedContext.App.Should().HaveSection<LoadingPage.SpinnerDemo>()
                .That.Should().HaveElement<LoadingPage.SpinnerDemo.Load>()
                    .That.AsButton().LetsClick();

            Assert.Throws<VafAssertionException>(() =>
                SharedContext.App.Should().HaveSection<LoadingPage.SpinnerDemo>()
                    .That.Should().HaveElement<LoadingPage.SpinnerDemo.LoadCount>()
                        .That.AsCounterField().Should().HaveValue("1"));
        }

        [Test]
        public void LateMessageIsWaitedFor()
        {
            // The message appears two seconds after the click; HaveElement waits for it
            SharedContext.App.Should().HaveSection<LoadingPage.LateDemo>()
                .That.Should().HaveElement<LoadingPage.LateDemo.Show>()
                    .That.AsButton().LetsClick()
                .AndAlso().Should().HaveElement<LoadingPage.LateDemo.Message>()
                    .That.AsLabelText().Should().HaveValue("Here I am, two seconds later.");
        }
    }
}
  • An element that is not there yet — HaveElement and HaveSection wait until it is present and visible, up to 10 seconds by default. LateMessageIsWaitedFor needs nothing more.
  • An element that is there, but not finished — the counter shows 0 from the start. HaveElement finds it at once, and the check reads it before the results arrive. WithoutTheLoaderTheCheckIsTooEarly shows it: the check fails, as the test expects.
  • The loader — LetsUseLoader<LoadingPage.Spinner>() tells VAF to wait for the spinner to disappear before it looks for any element. Then the same check gets 1. The loader is set for one browser: a new browser starts without it. If your application uses the same spinner everywhere, set it once in SharedContext.LetsStartTheBrowser.
4

Timeouts

VAF waits up to 10 seconds for an element and up to 60 seconds for the loader. To change them, set the environment variables VAF_ElementWaitTimeout and VAF_LoaderWaitTimeout, in seconds — in PowerShell before dotnet test:

$env:VAF_ElementWaitTimeout = "20"
dotnet test

In GitHub Actions or another pipeline, set them in the job's environment. VAF also reads them from the appSettings of an app.config, but on .NET 10 the test host often does not load the test project's app.config; environment variables work everywhere.

5

A report for every test, a screenshot for every failure

VAF writes every action and check into a log while the test runs. Let TestBase turn it into an HTML page after each test, and take a screenshot when the test has failed. Replace TestBase.cs in Common:

FirstVafTest/Common/TestBase.cs On GitHub
using NUnit.Framework;
using NUnit.Framework.Interfaces;
using VisionAutomationFramework;

namespace FirstVafTest.Common
{
    // The base of every test class: the browser starts before each test and closes after it.
    // A test class adds only its own [SetUp], usually one that opens its page; NUnit runs this
    // [SetUp] first, then the one of the test class.
    public abstract class TestBase
    {
        [SetUp]
        public void StartTheBrowser() => SharedContext.LetsStartTheBrowser();

        [TearDown]
        public void ReportAndClose()
        {
            try
            {
                // A failed test leaves a screenshot of the page as it was, attached to the result
                if (TestContext.CurrentContext.Result.Outcome.Status == TestStatus.Failed)
                {
                    SharedContext.Browser?.LetsTakeScreenshot();
                }

                // Every test leaves an HTML report of what VAF did, step by step
                Testing.ConvertLogToHtml(TestContext.CurrentContext.WorkDirectory,
                                         TestContext.CurrentContext.Test.Name);
            }
            finally
            {
                // Close the browser even when the report cannot be written
                SharedContext.LetsCloseTheBrowser();
            }
        }
    }
}

Every test class gets this without a change, because they all derive from TestBase. The report, VAF_Log_<test>_<time>.html, is written to the test output folder (bin\Debug\net10.0). The screenshot goes there too, and NUnit attaches it to the result of the failed test, so you can open it from Test Explorer.

6

Run it, then break it

Run all tests: twenty pass. In ResultsAreReadAfterTheSpinnerIsGone, expect "Beta (load 1)" and run it again. It fails; open the output folder:

  • the HTML report lists every step the test took — the click, the wait for the spinner, each check — and marks the one that failed, with the expected and the actual value;
  • the screenshot shows the page at that moment: the results are there, the first one is Alpha.

What you have now

A suite with page objects, shared steps and tests, which waits for slow pages, reports every run and shows you each failure — the same structure scales to a real application. Where to go from here:

  • Point it at your own application: one AppPage, then a page object per screen, then the steps.
  • VAF-Demo — the same structure for every practice page, and longer scenarios made only of steps.
  • Practice pages — tables, windows, alerts and more, to try on your own.
  • Guide — the reference for everything you used here.

Compare with the reference code

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