Going Multi-Platform – Adding a Mac to the Grid: Part 2

Going Multi-Platform – Adding a Mac to the Grid: Part 2

This entry is part 5 of 5 in the series Building a WebDriver Grid

Last time I discussed the differences in setting up a Mac as a node on a Selenium grid. That is not even half the battle however as I also need to get the supporting code in the framework and find out if I have to work around any differences in the WebDriver implementations. Back in January I wrote some tests checking that the framework was launching the correct browser and getting this working properly. I enjoy the challenge of writing code to test other people’s code, but it is easy to forget that my code is just as prone to bugs as anyone else’s and why should I expect anyone else to write tests for their code if I don’t do the same.

More Browsers

Ok so technically I am adding a new platform with some of the same browsers as before. I probably should think about structuring the browser options. Perhaps

Browser : Chrome, Edge, Firefox, InternetExplorer, Opera, Safari

OS: Android, IOS, Linux, MacOS, Windows

Environment: Local, Grid, BrowserStack, SauceLabs

With my C# implementation I will plan this in from the beginning, but for this I will just add three more options to my

DriverType enum.

CHROME_MACOS("Macintosh; Intel Mac OS X\\b.*?\\bChrome", "chrome"),

FIREFOX_MACOS("Macintosh; Intel Mac OS X\\b.*?\\bGecko/.*?\\bFirefox/", "firefox"),

SAFARI_MACOS("Macintosh; Intel Mac OS X\\b.*?\\bVersion/.*?\\bSafari/", "safari"),

Of course I didn’t have a clue for the regex patterns until I could get some debug on a working call.

The good news of course is that the default for the DriverFactoryTests class is to test all the browsers in this enum so no code changes were required. I was however grateful for all my examples of how to filter the tests.

DriverManagers

SafariDriverManager should be easy enough. A standard implementation with only a remote call.

package tech.alexontest.poftutor.infrastructure.driver;

import org.openqa.selenium.remote.RemoteWebDriver;
import org.openqa.selenium.safari.SafariOptions;

public final class SafariDriverManager extends AbstractDriverManager implements WebDriverManager {

    private final boolean isLocal;

    private final boolean isHeadless;

    SafariDriverManager(final boolean isLocal, final boolean isHeadless) {
        this.isLocal = isLocal;
        this.isHeadless = isHeadless;
    }

    @Override
    public void startService() {
        if (isLocal) {
            throw new UnsupportedOperationException("Local running is only supported on Microsoft Windows."
                    + " Please use the grid.");
        }
    }

    @Override
    public void stopService() {
        if (isLocal) {
            throw new UnsupportedOperationException("Local running is only supported on Microsoft Windows."
                    + " Please use the grid.");
        }
    }

    @Override
    public String createDriver() {
        final SafariOptions options = new SafariOptions();
        // add additional options here as required
        if (!isLocal) {
            setDriver(new RemoteWebDriver(getGridUrl(), options));
            options.setCapability("platform", "MAC");
        } else {
            if (isHeadless) {
                throw new UnsupportedOperationException("Safari does not support headless testing.");
            }
        }
        System.out.println("SafariDriver Started");
        get().manage().window().maximize();
        return options.getBrowserName();
    }
}
Safari up and running
I can now test how well Safari works

ChromeDriverManager and FirefoxDriverManager however are already rather complex classes. This whole DriverFactory system needs something of a refactor anyway so I’m not going to change too much for now. I obviously need to pass in a parameter for the platform. I was initially going to use another boolean, but more future proof is to use WebDriver’s own Platform  enum. In theory you might imagine that this would make platform calls easy and reliable, although as you will see, the was just imagination…..

Starting with the easy one.

package tech.alexontest.poftutor.infrastructure.driver;

import org.openqa.selenium.Platform;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeDriverService;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.remote.RemoteWebDriver;

import java.io.File;
import java.io.IOException;

public final class ChromeDriverManager extends AbstractDriverManager implements WebDriverManager {

    private ChromeDriverService chromeDriverService;

    private final File chromedriverExe;

    private final boolean isLocal;

    private final boolean isHeadless;

    private final Platform platform;

    ChromeDriverManager(final boolean isLocal, final boolean isHeadless) {
        this(isLocal, isHeadless, Platform.WIN10);
    }

    ChromeDriverManager(final boolean isLocal, final boolean isHeadless, final Platform platform) {
        final String path = getClass().getClassLoader().getResource("chromedriver.exe").getPath();
        chromedriverExe = new File(path);
        System.setProperty("webdriver.chrome.driver", path);
        this.isLocal = isLocal;
        this.isHeadless = isHeadless;
        this.platform = platform;
    }

    @Override
    public void startService() {
        if (!isLocal) {
            return;
        }
        if (null == chromeDriverService) {
            try {
                chromeDriverService = new ChromeDriverService.Builder()
                        .usingDriverExecutable(chromedriverExe)
                        .usingAnyFreePort()
                        .build();
                chromeDriverService.start();
            } catch (final IOException e) {
                e.printStackTrace();
            }
            System.out.println("ChromeDriverService Started");
        }
    }

    @Override
    public void stopService() {
        if (null != chromeDriverService && chromeDriverService.isRunning()) {
            chromeDriverService.stop();
            System.out.println("ChromeDriverService Stopped");
        }
    }

    @Override
    public String createDriver() {
        final ChromeOptions options = new ChromeOptions()
                .setHeadless(isHeadless)
                .addArguments("--test-type", "--start-maximized");
        options.setCapability("platform", platform);
        // add additional required options here
        if (!isLocal) {
            setDriver(new RemoteWebDriver(getGridUrl(), options));
        } else {
            setDriver(new ChromeDriver(chromeDriverService, options));
        }
        System.out.println("ChromeDriver Started");
        return options.getBrowserName();
    }
}

So:

I overloaded the constructor to allow the passing in of a platform. I’ve left the default call to Windows for now. This should even allow to use different versions of Windows e.g. Win10, Win7 if needed.

Much as I would like to avoid using Capabilities completely, it seems that the only way to pass the platform is with options.setCapability("platform", platform); Something else to look at if I refactor this.

I also recently discovered the options.setHeadless(isHeadless) syntax so I’ve used that instead of the if conditional.

Chrome on MacOS
Chrome on MacOS

Now in principal the same changes should apply

Thank goodness for those tests

I’m sure I would have found this out anyway, but running my tests quickly identified a problem here. It seems that requesting Firefox on Windows 10 “WIN10” fails due to a pretty longstanding bug in GeckoDriver.

After a little tweaking to the DriverTypes enum to ensure that they only pass when the Windows browsers launch on Windows.

CHROME("^(?!.*?\\bOPR/\\b)^(?!.*?\\bEdge/\\b).*?\\bWindows NT.*?\\bChrome/\\b.*?\\bSafari/\\b.*$", "chrome"),

and

FIREFOX("Windows NT\\b.*?\\bGecko/.*?\\bFirefox/", "firefox"),

I spent a silly amount of time trying to fix this before coding around it. I would have saved myself a lot of time checking the open issues earlier. Calling without a platform set gets me the Windows browser half the time, but that is the best I can do. Basically I can either have reliable Windows Firefox or MacOS, but not both on the same hub. This is annoying.

    @Override
    public String createDriver() {
        final FirefoxOptions options = new FirefoxOptions()
                .setHeadless(isHeadless)
                .setLogLevel(FirefoxDriverLogLevel.ERROR);
        //to stop the debug spam
        // add additional options here as required
        if (!isLocal) {
            if (!Platform.WIN10.toString().equals(platform)) {
                // This is messy but sending platform WIN10 fails so only set for MacOs
                options.setCapability("platform", platform);
            }
            setDriver(new RemoteWebDriver(getGridUrl(), options));
        } else {
            setDriver(new FirefoxDriver(geckoDriverService, options));
        }
        System.out.println("FirefoxDriver Started");
        return options.getBrowserName();
    }

That works some of the time.

But it shows that I also need to tweek my DriverTypes Enum to ensure that it fails if the wrong platform is launched. Did I mention that I hate regex?

Firefox on MacOS
Firefox on MacOS

FIREFOX("Windows NT\\b.*?\\bGecko/.*?\\bFirefox/", "firefox"),

 

That’s it for this time. Next time I look at whether the tests will pass, and whether any problems are due to my site or unusual WebDriver implementations. Surely Chrome and Firefox will still be ok on MacOS, but what about Safari?

Progress made:

  • Chrome, Firefox and Safari launching on MacOS on the grid and all passing the DriverFactory tests. (Yay!)
  • This breaks my ability to get a guaranteed Windows Firefox instance. (Boo!)

Lessons learnt:

  • When things are not working as they should, remember to check for known bugs before spending ages trying to diagnose it.
  • Even then, sometimes published workarounds just plain don’t work.
  • I need to work harder to filter out the debug spam from Firefox or employ a proper logger (which I really should) as it is spewing out heaps now.
Series Navigation<< Going Multi-Platform – Adding a Mac to the Grid: Part 1
Comments are closed.