diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 22e1e235..8291ca04 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -60,6 +60,7 @@ jobs: # playwright browser versions are pinned to playwright version {runner: playwright, runtime: firefox, playwright-version: 1.22.0, node-version: 18}, {runner: playwright, runtime: chrome, playwright-version: 1.22.0, node-version: 18}, + {runner: playwright, runtime: safari, playwright-version: 1.22.0, node-version: 18}, ] steps: - uses: actions/checkout@v2 @@ -86,8 +87,11 @@ jobs: if: ${{ matrix.test-config.runner == 'playwright' }} run: | pip install playwright==${{ matrix.test-config.playwright-version }} - # TODO: install only browsers that are required - python -m playwright install --with-deps + RUNTIME=${{ matrix.test-config.runtime }} + RUNTIME=${RUNTIME//safari/webkit} + RUNTIME=${RUNTIME//chrome/chromium} + + python -m playwright install "${RUNTIME}" --with-deps - name: Install firefox uses: browser-actions/setup-firefox@latest diff --git a/pytest_pyodide/__init__.py b/pytest_pyodide/__init__.py index 415170f8..a4aa4afa 100644 --- a/pytest_pyodide/__init__.py +++ b/pytest_pyodide/__init__.py @@ -9,6 +9,7 @@ PlaywrightChromeWrapper, PlaywrightFirefoxRunner, PlaywrightFirefoxWrapper, + PlaywrightSafariRunner, PlaywrightWrapper, SeleniumChromeRunner, SeleniumChromeWrapper, @@ -29,6 +30,7 @@ "NodeRunner", "PlaywrightChromeRunner", "PlaywrightFirefoxRunner", + "PlaywrightSafariRunner", "SeleniumChromeRunner", "SeleniumFirefoxRunner", "set_webdriver_script_timeout", diff --git a/pytest_pyodide/constants.py b/pytest_pyodide/constants.py new file mode 100644 index 00000000..81ad8479 --- /dev/null +++ b/pytest_pyodide/constants.py @@ -0,0 +1 @@ +RUNTIMES = ["firefox", "chrome", "node", "safari"] diff --git a/pytest_pyodide/fixture.py b/pytest_pyodide/fixture.py index 4638ce50..36cbb3a4 100644 --- a/pytest_pyodide/fixture.py +++ b/pytest_pyodide/fixture.py @@ -8,6 +8,7 @@ NodeRunner, PlaywrightChromeRunner, PlaywrightFirefoxRunner, + PlaywrightSafariRunner, SeleniumChromeRunner, SeleniumFirefoxRunner, _BrowserBaseRunner, @@ -16,10 +17,14 @@ from .utils import parse_driver_timeout, set_webdriver_script_timeout +# FIXME: Using `session` scope can reduce the number of playwright context generation. +# However, generating too many browser contexts in a single playwright context +# sometimes hang when closing the context. @pytest.fixture(scope="module") -def playwright_browsers(request): +def playwright_session(request): if request.config.option.runner.lower() != "playwright": - yield {} + yield None + else: # import playwright here to allow running tests without playwright installation try: @@ -30,27 +35,37 @@ def playwright_browsers(request): returncode=1, ) - with sync_playwright() as p: - try: - chromium = p.chromium.launch( - args=[ - "--js-flags=--expose-gc", - ], - ) - firefox = p.firefox.launch() - # webkit = p.webkit.launch() - except Exception as e: - pytest.exit(f"playwright failed to launch\n{e}", returncode=1) - try: - yield { - "chrome": chromium, - "firefox": firefox, - # "webkit": webkit, - } - finally: - chromium.close() - firefox.close() - # webkit.close() + p = sync_playwright().start() + yield p + p.stop() + + +@pytest.fixture(scope="module") +def playwright_browser(request, playwright_session, runtime): + if request.config.option.runner.lower() != "playwright": + yield None + else: + try: + match runtime: + case "chrome": + browser = playwright_session.chromium.launch( + args=[ + "--js-flags=--expose-gc", + ], + ) + case "firefox": + browser = playwright_session.firefox.launch() + case "safari": + browser = playwright_session.webkit.launch() + case "node": + browser = None + except Exception as e: + pytest.exit(f"playwright failed to launch\n{e}", returncode=1) + try: + yield browser + finally: + if browser is not None: + browser.close() @contextlib.contextmanager @@ -60,7 +75,7 @@ def selenium_common( web_server_main, load_pyodide=True, script_type="classic", - browsers=None, + playwright_browser=None, ): """Returns an initialized selenium object. @@ -78,6 +93,7 @@ def selenium_common( ("selenium", "node"): NodeRunner, ("playwright", "firefox"): PlaywrightFirefoxRunner, ("playwright", "chrome"): PlaywrightChromeRunner, + ("playwright", "safari"): PlaywrightSafariRunner, ("playwright", "node"): NodeRunner, } @@ -91,7 +107,7 @@ def selenium_common( server_hostname=server_hostname, server_log=server_log, load_pyodide=load_pyodide, - browsers=browsers, + playwright_browser=playwright_browser, script_type=script_type, dist_dir=dist_dir, ) @@ -102,9 +118,9 @@ def selenium_common( @pytest.fixture(scope="function") -def selenium_standalone(request, runtime, web_server_main, playwright_browsers): +def selenium_standalone(request, runtime, web_server_main, playwright_browser): with selenium_common( - request, runtime, web_server_main, browsers=playwright_browsers + request, runtime, web_server_main, playwright_browser=playwright_browser ) as selenium: with set_webdriver_script_timeout( selenium, script_timeout=parse_driver_timeout(request.node) @@ -116,13 +132,13 @@ def selenium_standalone(request, runtime, web_server_main, playwright_browsers): @pytest.fixture(scope="module") -def selenium_esm(request, runtime, web_server_main, playwright_browsers): +def selenium_esm(request, runtime, web_server_main, playwright_browser): with selenium_common( request, runtime, web_server_main, load_pyodide=True, - browsers=playwright_browsers, + playwright_browser=playwright_browser, script_type="module", ) as selenium: with set_webdriver_script_timeout( @@ -136,14 +152,14 @@ def selenium_esm(request, runtime, web_server_main, playwright_browsers): @contextlib.contextmanager def selenium_standalone_noload_common( - request, runtime, web_server_main, playwright_browsers, script_type="classic" + request, runtime, web_server_main, playwright_browser, script_type="classic" ): with selenium_common( request, runtime, web_server_main, load_pyodide=False, - browsers=playwright_browsers, + playwright_browser=playwright_browser, script_type=script_type, ) as selenium: with set_webdriver_script_timeout( @@ -157,7 +173,7 @@ def selenium_standalone_noload_common( @pytest.fixture(scope="function") def selenium_webworker_standalone( - request, runtime, web_server_main, playwright_browsers, script_type + request, runtime, web_server_main, playwright_browser, script_type ): # Avoid loading the fixture if the test is going to be skipped if runtime == "firefox" and script_type == "module": @@ -167,27 +183,27 @@ def selenium_webworker_standalone( pytest.skip("no support in node") with selenium_standalone_noload_common( - request, runtime, web_server_main, playwright_browsers, script_type=script_type + request, runtime, web_server_main, playwright_browser, script_type=script_type ) as selenium: yield selenium @pytest.fixture(scope="function") -def selenium_standalone_noload(request, runtime, web_server_main, playwright_browsers): +def selenium_standalone_noload(request, runtime, web_server_main, playwright_browser): """Only difference between this and selenium_webworker_standalone is that this also tests on node.""" with selenium_standalone_noload_common( - request, runtime, web_server_main, playwright_browsers + request, runtime, web_server_main, playwright_browser ) as selenium: yield selenium # selenium instance cached at the module level @pytest.fixture(scope="module") -def selenium_module_scope(request, runtime, web_server_main, playwright_browsers): +def selenium_module_scope(request, runtime, web_server_main, playwright_browser): with selenium_common( - request, runtime, web_server_main, browsers=playwright_browsers + request, runtime, web_server_main, playwright_browser=playwright_browser ) as selenium: yield selenium @@ -214,7 +230,7 @@ def selenium(request, selenium_module_scope): @pytest.fixture(scope="function") -def console_html_fixture(request, runtime, web_server_main, playwright_browsers): +def console_html_fixture(request, runtime, web_server_main, playwright_browser): if runtime == "node": pytest.skip("no support in node") @@ -224,7 +240,7 @@ def console_html_fixture(request, runtime, web_server_main, playwright_browsers) runtime, web_server_main, load_pyodide=False, - browsers=playwright_browsers, + playwright_browser=playwright_browser, ) as selenium: selenium.goto( f"http://{selenium.server_hostname}:{selenium.server_port}/console.html" diff --git a/pytest_pyodide/hook.py b/pytest_pyodide/hook.py index f718e332..7c13a706 100644 --- a/pytest_pyodide/hook.py +++ b/pytest_pyodide/hook.py @@ -10,10 +10,9 @@ pytest_pycollect_makemodule as orig_pytest_pycollect_makemodule, ) +from .constants import RUNTIMES from .utils import parse_xfail_browsers -RUNTIMES = ["firefox", "chrome", "node"] - def pytest_configure(config): @@ -106,9 +105,12 @@ def pytest_pycollect_makemodule(module_path: Path, path: Any, parent: Any) -> No def pytest_generate_tests(metafunc: Any) -> None: if "runtime" in metafunc.fixturenames: runtime = metafunc.config.option.runtime + runner = metafunc.config.option.runner if runtime == "all": runtime = RUNTIMES + if runner == "selenium": + runtime = runtime.remove("safari") metafunc.parametrize("runtime", [runtime], scope="module") diff --git a/pytest_pyodide/runner.py b/pytest_pyodide/runner.py index 1080447c..a563abfd 100644 --- a/pytest_pyodide/runner.py +++ b/pytest_pyodide/runner.py @@ -358,15 +358,15 @@ def urls(self): class _PlaywrightBaseRunner(_BrowserBaseRunner): - def __init__(self, browsers, *args, **kwargs): - self.browsers = browsers + def __init__(self, playwright_browser, *args, **kwargs): + self.playwright_browser = playwright_browser super().__init__(*args, **kwargs) def goto(self, page): self.driver.goto(page) def get_driver(self): - return self.browsers[self.browser].new_page() + return self.playwright_browser.new_page() def set_script_timeout(self, timeout): # playwright uses milliseconds for timeout @@ -445,6 +445,10 @@ class PlaywrightFirefoxRunner(_PlaywrightBaseRunner): browser = "firefox" +class PlaywrightSafariRunner(_PlaywrightBaseRunner): + browser = "safari" + + class NodeRunner(_BrowserBaseRunner): browser = "node" diff --git a/pytest_pyodide/utils.py b/pytest_pyodide/utils.py index 7da9bcb8..2b9e1e1f 100644 --- a/pytest_pyodide/utils.py +++ b/pytest_pyodide/utils.py @@ -6,6 +6,8 @@ import pytest +from .constants import RUNTIMES + @contextlib.contextmanager def set_webdriver_script_timeout(selenium, script_timeout: float | None): @@ -47,7 +49,7 @@ def maybe_skip_test(item, dist_dir, delayed=False): loading the selenium_standalone fixture which takes a long time. """ - browsers = "|".join(["firefox", "chrome", "node"]) + browsers = "|".join(RUNTIMES) skip_msg = None # Testing a package. Skip the test if the package is not built. diff --git a/tests/test_marker.py b/tests/test_marker.py index 81544d8f..017f38d6 100644 --- a/tests/test_marker.py +++ b/tests/test_marker.py @@ -4,7 +4,10 @@ @pytest.mark.xfail_browsers( - node="Should xfail", firefox="Should xfail", chrome="Should xfail" + node="Should xfail", + firefox="Should xfail", + chrome="Should xfail", + safari="Should xfail", ) @run_in_pyodide def test_xfail_browser(selenium):