Giter Site home page Giter Site logo

Inside pytest_playwright plugin browser_context_args() fixture is session scoped, but should be function scoped. about playwright-pytest HOT 4 CLOSED

RomanLeca avatar RomanLeca commented on June 11, 2024
Inside pytest_playwright plugin browser_context_args() fixture is session scoped, but should be function scoped.

from playwright-pytest.

Comments (4)

RomanLeca avatar RomanLeca commented on June 11, 2024

I put together a working solution that leverages the plugin as much as currently possible:
conftest.py:

@pytest.fixture
def device_name(request, device):
    """
    Collect all specified devices for a test from a pytest.mark.parametrize("device_name", [..]) marker
    """
    try:
        return request.param
    except AttributeError:
        return device


@pytest.fixture
def more_context_args(playwright, browser_name, device_name):
    """
    Alter context arguments on a test level
    """
    context_args = {}
    if device_name not in playwright.devices:
        context_args.setdefault("viewport", {"width": 1920, "height": 1080})
    else:
        context_args.update(playwright.devices[device_name])

    # handle staging auth
    context_args.setdefault("http_credentials", {"username": AUTH_USERNAME, "password": AUTH_PASSWORD})

    return context_args


def pytest_collection_modifyitems(items, config):
    """
    This is a standard pytest hook that runs after tests have been identified and collected, but need further filtering
    We will leverage this hook to filter out incompatible combinations of browsers and devices.
    """
    # since this is collection stage, we don't have access to fixtures yet, so we'll have to start and stop playwright
    # manually to access the devices list
    playwright_context = sync_playwright().start()
    devices = playwright_context.devices

    # we're going to collect tests that run on incompatible combinations here
    deselected_items = []

    for item in items.copy():
        # Get browser name from the plugin browser_name fixture
        browser_name = item.callspec.getparam("browser_name")

        # Handle only_browser and skip_browser plugin markers to prevent the SKIPPED tests noise in the reporting
        only_browser_marker = item.get_closest_marker("only_browser")
        skip_browser_marker = item.get_closest_marker("skip_browser")
        if (only_browser_marker and browser_name != only_browser_marker.args[0]) or (
            skip_browser_marker and browser_name == skip_browser_marker.args[0]
        ):
            deselected_items.append(item)
            continue

        # Get device name from pytest.mark.parametrize('device_name',[...]) marker, if it's specified
        try:
            device_name = item.callspec.getparam("device_name")
        except ValueError:
            device_name = ""

        # Get device dictionary settings for specified devices
        device_info = None
        if device_name:
            device_info = devices[device_name]

        # Check invalid devices-browser combinations and mark them as deselected
        invalid_browser_device_combinations = [
                        (device_info and browser_name == "firefox" and device_info.get("is_mobile", False)),
            (device_info and browser_name == "webkit" and device_info["default_browser_type"] != "webkit"),
            (device_info and browser_name == "chromium" and device_info["default_browser_type"] != "chromium"),
            (browser_name == "firefox" and "Chrome" in device_name),
            (browser_name == "firefox" and "Edge" in device_name),
            (browser_name == "chromium" and "Safari" in device_name),
            (browser_name == "firefox" and "Safari" in device_name)
        ]
        if any(invalid_browser_device_combinations):
            deselected_items.append(item)
            continue
    # filter out the deselected items and leave only relevant ones
    selected_items = [i for i in items if i not in deselected_items]
    config.hook.pytest_deselected(items=deselected_items)
    playwright_context.stop()
    items[:] = selected_items


@pytest.fixture
def context(
    browser: Browser,
    browser_context_args: Dict,
    pytestconfig: Any,
    request: pytest.FixtureRequest,
    more_context_args: Dict,
) -> Generator[BrowserContext, None, None]:
    pages: List[Page] = []

    browser_context_args.update(more_context_args)
    context = browser.new_context(**browser_context_args)
    # Collect every page that's opened in the browser context into the pages list.
    context.on("page", lambda page: pages.append(page))

    tracing_option = pytestconfig.getoption("--tracing")
    capture_trace = tracing_option in ["on", "retain-on-failure"]
    if capture_trace:
        context.tracing.start(
            title=slugify(request.node.nodeid),
            screenshots=True,
            snapshots=True,
            sources=True,
        )

    yield context
    # If requst.node is missing rep_call, then some error happened during execution
    # that prevented teardown, but should still be counted as a failure
    failed = request.node.rep_call.failed if hasattr(request.node, "rep_call") else True

    if capture_trace:
        retain_trace = tracing_option == "on" or (failed and tracing_option == "retain-on-failure")
        if retain_trace:
            trace_path = _build_artifact_test_folder(pytestconfig, request, "trace.zip")
            context.tracing.stop(path=trace_path)
        else:
            context.tracing.stop()

    screenshot_option = pytestconfig.getoption("--screenshot")
    capture_screenshot = screenshot_option == "on" or (failed and screenshot_option == "only-on-failure")
    # handle multi-page tests, by saving a screenshot of each individual page
    if capture_screenshot:
        for index, page in enumerate(pages):
            human_readable_status = "failed" if failed else "finished"
            screenshot_path = _build_artifact_test_folder(
                pytestconfig, request, f"test-{human_readable_status}-{index+1}.png"
            )
            try:
                page.screenshot(timeout=5000, path=screenshot_path)
            except Error:
                pass

    context.close()

test_xxx.py file:

pytest.mark.parametrize(
        "device_name",
        [
            "Desktop Chrome",
            "Desktop Edge",
            "Desktop Firefox",
            "Desktop Safari",
            "iPad Pro 11",
            "Galaxy Tab S4 landscape",
            "iPhone 13 Pro landscape",
            "iPhone 12 Pro Max",
            "Pixel 5",
        ],
        indirect=True,
    )
def test_xxx(reachmd_homepage):
    pass

CLI: pytest tests --browser chromium --browser firefox --browser webkit -k xxx --co

Output:

<Function test_xxx[chromium-Desktop Chrome]>
<Function test_xxx[chromium-Desktop Edge]>
<Function test_xxx[chromium-iPad Pro 11]>
<Function test_xxx[chromium-Galaxy Tab S4 landscape]>
<Function test_xxx[chromium-iPhone 13 Pro landscape]>
<Function test_xxx[chromium-iPhone 12 Pro Max]>
<Function test_xxx[chromium-Pixel 5]>
<Function test_xxx[firefox-Desktop Firefox]>
<Function test_xxx[webkit-Desktop Safari]>
<Function test_xxx[webkit-iPad Pro 11]>
<Function test_xxx[webkit-iPhone 13 Pro landscape]>
<Function test_xxx[webkit-iPhone 12 Pro Max]>

from playwright-pytest.

RomanLeca avatar RomanLeca commented on June 11, 2024

Possible improvements(overkill?) to this:

  • Have a way to force default test execution on whatever desktop browsers are specified in the CLI, alongside the device_name marker values, if any. That will remove the need to specify "Desktop <browser>" manually for tests that need to run on both.
  • Add any device specified in the CLI via --device to the device_name params list and run the test on all devices specified via marker or CLI param

from playwright-pytest.

mxschmitt avatar mxschmitt commented on June 11, 2024

I created the following test to reproduce your use, but for me it was working, what did I do differently?

def test_different_browser_context_args_per_module(testdir: pytest.Testdir) -> None:
    module_a_path = testdir.mkpydir("moduleA")
    module_a_submodule_path = testdir.mkpydir("moduleA/submodule")
#     module_a_path.join("conftest.py").write("""
# import pytest

# @pytest.fixture(scope="session")
# def browser_context_args():
#     return {"user_agent": "module_a"}
# """)
    module_a_submodule_path.join("conftest.py").write("""
import pytest

@pytest.fixture(scope="session")
def browser_context_args(browser_context_args):
    browser_context_args.setdefault("user_agent", "module_a_submodule")
    return browser_context_args
""")
    module_a_submodule_path.join("test_foo.py").write(
        """
def test_browser_context_args1(page):
    assert page.evaluate("window.navigator.userAgent") == "module_a_submodule"
def test_browser_context_args2(page):
    assert page.evaluate("window.navigator.userAgent") == "module_a_submodule"
    """
    )
    result = testdir.runpytest()
    result.assert_outcomes(passed=2)

You can put it inside here to run the test. For the other suggestions above I recommend creating own feature requests, so that we track here only the bug report about the wrong scope.

from playwright-pytest.

mxschmitt avatar mxschmitt commented on June 11, 2024

Closing as per above. Please re-file if you are still encountering issues.

from playwright-pytest.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.