# Share the browser between Playwright and Selenium tests

> Launching a browser for every test is slow. Use pytest-playwright's session-scoped browser with a new context per test, or a session-scoped Selenium driver, and a separate CI job.

Browser tests are the slowest tests most suites have. Some of that is unavoidable: the browser has to load and render pages. But a large part is often overhead the test doesn't need: launching a new browser process for every test, and logging in through the UI before every test.

Launching Chromium or Firefox takes from a few hundred milliseconds to seconds, depending on the machine. A suite of 200 browser tests that each launch their own browser pays that 200 times. A fresh *context* (Playwright) or a cleared session (Selenium) gives the same isolation for a fraction of the cost.

## Why it's slow

- **A browser per test.** A function-scoped fixture that calls `playwright.chromium.launch()` or `webdriver.Chrome()` starts and stops a whole browser process for every test.
- **Logging in through the UI** in every test: loading the login page, filling the form, waiting for the redirect.
- **Fixed waits**, like `time.sleep(2)` or `page.wait_for_timeout(2000)`, instead of waiting for the element or state the test needs. See [tests are sleeping](https://flakytest.dev/guides/sleep-in-tests/).
- **Testing logic through the browser** that a unit or view test could check in milliseconds.

## Playwright: one browser, a new context per test

pytest-playwright already does the right thing if you use its fixtures. `browser` (along with `playwright` and `browser_type`) is session-scoped: one browser for the whole run. `context` and `page` are function-scoped: each test gets a new browser context, with its own cookies, local storage and cache, and a new page in it.

```python
# Before: a new browser for every test
import pytest
from playwright.sync_api import sync_playwright


@pytest.fixture
def page():
    with sync_playwright() as p:
        browser = p.chromium.launch()
        page = browser.new_page()
        yield page
        browser.close()
```

```python
# After: delete the fixture above and use pytest-playwright's `page`
from playwright.sync_api import Page, expect


def test_signup(page: Page):
    page.goto("/signup/")  # relative to --base-url
    page.get_by_label("Email").fill("ada@example.com")
    page.get_by_role("button", name="Sign up").click()
    expect(page.get_by_text("Check your inbox")).to_be_visible()
```

Configure the browser with fixtures and command-line options instead of launching it yourself: `browser_type_launch_args` for launch options, `browser_context_args` for context options (viewport, locale, `base_url`), and `--browser`, `--headed`, `--base-url` and `--tracing retain-on-failure` on the command line.

### Log in once, reuse the storage state

Save the cookies and local storage of a logged-in session once, and start every context from it:

```python
import pytest


@pytest.fixture(scope="session")
def auth_state(browser, tmp_path_factory, base_url):
    path = tmp_path_factory.mktemp("auth") / "state.json"
    context = browser.new_context(base_url=base_url)
    page = context.new_page()
    page.goto("/login/")
    page.get_by_label("Email").fill("ada@example.com")
    page.get_by_label("Password").fill("correct horse battery staple")
    page.get_by_role("button", name="Log in").click()
    page.wait_for_url("**/dashboard/")
    context.storage_state(path=path)
    context.close()
    return path


@pytest.fixture(scope="session")
def browser_context_args(browser_context_args, auth_state):
    return {**browser_context_args, "storage_state": auth_state}
```

This only works if the session stays valid for the whole run: the user and its session must not be deleted between tests. Keep the tests that are about logging in on a context without the stored state.

## Selenium: a session-scoped driver

Selenium has no contexts, so share the driver and clean up between tests:

```python
import pytest
from selenium import webdriver


@pytest.fixture(scope="session")
def browser():
    options = webdriver.ChromeOptions()
    options.add_argument("--headless=new")
    driver = webdriver.Chrome(options=options)
    yield driver
    driver.quit()


@pytest.fixture
def driver(browser, live_server):
    browser.get(live_server.url)
    yield browser
    browser.delete_all_cookies()
    browser.execute_script("window.localStorage.clear(); window.sessionStorage.clear();")
```

`delete_all_cookies()` and the storage calls only reach the domain of the page that's currently open, which is why the fixture opens the site first. If tests visit several domains, clear each one, or fall back to a new driver for those tests. For locating elements the way users do, see [Selenium Testing Library](https://blog.pecar.me/selenium-testing-library/).

## Run browser tests in their own CI job

Even with a shared browser, browser tests are much slower than the rest. Mark them and give them their own job, so the fast tests report back first:

```python
# conftest.py
import pytest


def pytest_collection_modifyitems(items):
    for item in items:
        if "page" in item.fixturenames or "driver" in item.fixturenames:
            item.add_marker(pytest.mark.e2e)
```

```console
$ pytest -m "not e2e"              # the fast job
$ pytest -m e2e --numprocesses auto  # the browser job, in parallel
```

Register the marker under `markers` in your pytest configuration. With pytest-xdist, each worker launches one browser and keeps it for its share of the tests. And move checks that don't need a browser (validation rules, permissions, calculations) into unit or view tests; more on that in [end-to-end testing best practices](https://flakytest.dev/guides/end-to-end-testing-best-practices/).

## Keep the tests correct

- A fresh browser context per test keeps cookies and storage isolated. Never share a `context` or `page` between tests unless you reset them.
- With a shared Selenium driver, state you don't clear (cookies on other domains, IndexedDB, open windows, service workers) leaks into the next test. Run the browser tests in random order now and then to catch it.
- A stored login state skips the login flow: keep at least one test that logs in through the UI.

## How flakytest finds it

flakytest puts time spent in Selenium, Playwright, splinter and pyppeteer in the *browser* category. When browser automation takes more than 2 seconds and 5% of test time, it suggests **Share the browser between tests**, with the tests that spend the most time in the browser and the lines of your code behind it. The browser's own processes aren't profiled, so their work shows up as time the test spends waiting on the driver. `flakytest report --test <test id>` shows how much of one test is setup (launching, logging in) and how much is the test itself.

## Further reading

- [Playwright: pytest plugin reference](https://playwright.dev/python/docs/test-runners)
- [Playwright: authentication and storage state](https://playwright.dev/python/docs/auth)
- [Selenium: working with cookies](https://www.selenium.dev/documentation/webdriver/interactions/cookies/)
