danielwertheim

danielwertheim


notes from a passionate developer

Share


Sections


Tags


Disclaimer

This is a personal blog. The opinions expressed here represent my own and not those of my employer, nor current or previous. All content is published "as is", without warranty of any kind and I don't take any responsibility and can't be liable for any claims, damages or other liabilities that might be caused by the content.

Selenium - PageObjects and Iframes

Was putting together some proof of concept automation tests using Specflow and Selenium today. In one scenario I had to reach fields which were nested two levels down in Iframes. For simplifying the switch of context for my PageObject I had to put together a small recursive extension method. Couldn’t find something about it in Selenium, so perhaps it could be of use for you to.

Basically the scenario is as follows. A first PageObject is initiated and navigates to an URL. When navigation is complete and the page has loaded, a click is performed on a simple button. The button in turn loads a modal with nested Iframes. This modal view is represented by another PageObject, which in turns uses members of the Selenium support NuGet to map fields in the PageObject to elements in the view.

Disclaimer, this is more a proof of concept sample so there are most likely room for improvements, and you are more then welcome to leave a comment with suggestion for improvements.

Lets start by having a look at the StartPage which so far only exposes methods for navigating to the page as well as opening the modal for registering for a new user account.

public class StartPage : PageObject
{
    [FindsBy(How = How.CssSelector, Using = ".registerAccountButton")]
    protected IWebElement RegisterAccountButton;

    public StartPage(IWebDriver driver) : base(driver) {}

    public virtual StartPage NavigateTo()
    {
        Driver.Navigate().GoToUrl(TestEnvironment.BuildUrl());

        return this;
    }

    public virtual RegisterAccountPartial OpenRegisterAccountModal()
    {
        RegisterAccountButton.Click();

        Driver.WaitUntil(() => Driver.TrySwitchToIframeWithChild(By.Id("firstname")));

        return new RegisterAccountPartial(Driver);
    }
}

The interesting code here lies in this line:

Driver.WaitUntil(() => Driver.TrySwitchToIframeWithChild(By.Id("firstname")));

It uses two extension methods I’ve put together: Driver.WaitUntil and Driver.TrySwitchToIframeWithChild. The first one isn’t very interesting as it merely wraps already existing functionality in Selenium. The second one however is a bit more interesting. But before looking at it, lets explain the problem. The RegisterAccountPartial, PageObject is using the FindsBy attribute to find controls in the view and map them to a property representing a certain IWebElement. Lets look at a slimmed down version of the RegisterAccountPartial, PageObject:

public class RegisterAccountPartial : PageObject
{
    [FindsBy(How = How.ClassName, Using = ".registerAccount")]
    protected IWebElement SubmitButton;

    [FindsBy(How = How.Id, Using = "firstname")]
    protected IWebElement FirstnameInput;

    [FindsBy(How = How.Id, Using = "lastname")]
    protected IWebElement LastnameInput;

    public RegisterAccountPartial(IWebDriver driver) : base(driver) {}

    public virtual string Firstname()
    {
        return FirstnameInput.Value();
    }

    public virtual RegisterAccountPartial WithFirstname(string value)
    {
        FirstnameInput.SendKeys(value);

        return this;
    }

    public virtual RegisterAccountPartial WithLastname(string value)
    {
        LastnameInput.SendKeys(value);

        return this;
    }

    public virtual void Submit()
    {
        SubmitButton.Click();
    }
}

Neither the SubmitButton nor the FirstnameInput will be mapped, and an exception will be thrown stating that the page does not contain any elements matching the selectors. The reason for this is that the Driver context is the outermost page, hence the iframes that holds the actual controls being targeted is not known to the driver. What you can do is to manually switch context to each iframe by using:

Driver.SwitchTo().Frame(By.Name("frame1"));

This was nothing I liked, since I had to work with a hierarchy of iframes, so instead, the idea was to write a recursive extension method, which tries to find a specific control in an iframe. And if found, switch context to that frame before initiating the RegisterAccountPartial (as shown above in StartPage.OpenRegisterAccountModal).

The method for switching context to the frame looks like this:

public static bool TrySwitchToIframeWithChild(this IWebDriver driver, By childBy)
{
    IWebElement el;
    if (TryFindElement(driver, childBy, out el))
        return true;

    foreach (var frame in driver.FindElements(By.TagName("iframe")))
    {
        driver.SwitchTo().Frame(frame);
        if (TrySwitchToIframeWithChild(driver, childBy))
            return true;
    }

    driver.SwitchTo().DefaultContent();
    return false;
}

public static bool TryFindElement(this ISearchContext ctx, By by, out IWebElement el)
{
    try
    {
        el = ctx.FindElement(by);
    }
    catch (Exception)
    {
        el = null;

        return false;
    }

    return true;            
}

The TryFindElement is a dirty hack, introduced just because FindElement throws an exception if no element can be found.

That’s it. You can now use this in e.g. your Specflow scenarios. E.g something like:

[Binding]
public class RegistrationSteps : StepsForVerificationOf<RegisterAccountPartial>
{
    protected override RegisterAccountPartial CreateUiObject()
    {
        return new StartPage(Driver)
            .NavigateTo()
            .OpenRegisterUser();
    }

    [Given]
    public void Given_I_have_filled_out_the_form_properly()
    {
        UiObject
            .WithFirstname("Daniel")
            .WithLastname("Wertheim");
    }
        
    [When]
    public void When_I_press_open_account()
    {
        UiObject.Submit();
    }
        
    [Then]
    public void Then_the_account_should_have_been_created()
    {
        //Some nice verification e.g
        //UiObject.SuccessNotification().Should().Be("something");
    }
}

That’s it for now. Happy coding.

Cheers,

//Daniel

View Comments