Multiple Browser testing with PlayWright in Python

Multiple Browser testing with PlayWright in Python

Introduction

Are you developing a new testing framework that needs to support multiple browsers? Or even worse - you have a testing framework and you need to expand the functionality by adding more browsers? In this article, I will try to introduce you to Playwright and show you how to easily set up your testing framework to support multiple browser execution. If you are new to the QA community you might not heard of Playwright, so first I'll explain in short notes what it is, so bare with me through the next boring paragraph.

What is Playwright?

Playwright is an open-source cross-browser automation framework for end-to-end testing. Technology is written in Node.js and supports Chrome, Firefox, Safari, Opera, and Edge browsers as well as Java, C#, Python, TypeScript, and JavaScipt languages. Except the great documentation and large community, it caught my attention with interesting features such as code generator, inspector, and trace-viewer. I will write about those functionalities on some other occasion, but if you are interested you can read more about it in the Playwright documentation. Now that we know what it is, let's get to work.

Starting the browser

We all know that in order to run browsers in selenium we have to install selenium webdriver locally. This should be a trivial thing, but it turns out to be more painful than expected, especially for browsers other than Chrome or Firefox. On the other hand, once Playwright is installed it is easy to launch the browser. Almost too easy. All you have to do is call the launcher and define a new page as shown in the example below:

from playwright.sync_api import sync_playwright

def run(playwright):
    firefox = playwright.firefox
    browser = firefox.launch()
    page = browser.new_page()

Define the browser in the fixtures

If you are familiar with pytest then you probably heard of fixtures. If not, fixtures are functions most commonly defined in the conftest.py file that are executed before or after the test run. That being said, we have the ability to define our browser in the fixture before any test is run and we will take advantage of that. Here is an example of how to launch a new page using fixtures:

@fixture(scope="session")
def browser() -> Browser:
    # Start playwright
    browser: Playwright = sync_playwright().start()
    # Launch browser
    return playwright.chromium.launch(headless=False)


@fixture(scope="session")
def context(browser) -> Generator[BrowserContext, None, None]:
    # Create new context
    yield browser.new_context()
    # Close context and browser
    context.close() 
    browser.close()


@fixture(scope="session")
def page(context) -> Page: 
    # Create new page
    return context.new_page()

This way, we only need to import page fixture in order for tests to work. The point of this article is not to show you how the fixtures work, but how we can run tests with multiple browsers. I'll return to the browser fixture and extend it a little bit.

from playwright.sync_api import Browser, BrowserType


@fixture(scope="session")
def browser() -> Browser:
    # Start playwright
    browser: Playwright = sync_playwright().start()
    # Get browser information from config.py
    browser_info: dict = get_browser()
    # Get launcher attributes and set the browser as defined in broser_info
    launcher: BrowserType = getattr(browser, browser_info["browser"])
    # Launch browser
    return launcher.launch(
        headless=False, channel=browser_info.get("channel")
    )

Where did all of this come from? We'll go line by line: We started Playwright like in the previous example, but now we are getting the browser_info dictionary. Function get_browser() will return - can you guess it - browser information.

Fetch the browser value from the config

This is how the get_browser() function looks like inside the config.py file:

def get_browser() -> dict:
    browser_list = {
        "chrome": {"browser": "chromium"},
        "firefox": {"browser": "firefox"},
        "safari": {"browser": "webkit"},
        "edge": {"browser": "chromium", "channel": "msedge"},
    }
    env_browser: str = os.getenv("BROWSER", "chrome")
    if env_browser not in browser_list:
        raise Exception("Invalid browser")

    return browser_list[env_browser]

If you are good with Python, you can see that we are fetching value from the .env file (can be Chrome, Firefox, Safari, or Edge) and based on the environment value, function will return the corresponding browser. One thing that is not intuitive is the Edge browser. There is a reason why it is an exception, and that's because the Edge browser uses the Chromium browser with msedge set as a channel, or in other words, Chromium can simulate the Edge browser if necessary.

Let's connect the dots

Once we got the info we need, we define a launcher which is a type of BrowserType. With the gettatr() method, we have the access to the browser's attributes and by passing the browser_info["browser"] to it, our launcher gets the values chromium, firefox, or webkit. One more thing we need to take care of when launching the launcher is how to set the channel (Edge example). When we return the launcher, we either set the channel if it exists or we don't do anything with the dictionary's get() method, since the default value it returns is None. Once all of this is set up, just define which browser you want to use in your .env file (BROWSER variable).

I hope You learned something useful today. Next time I will write an article on how to dockerize all of this as well and run the tests with Makefile.