From 604bbcc218473bffc4381a5adde463718aad419e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jim=20Br=C3=A4nnlund?= Date: Sun, 30 Oct 2022 23:52:40 +0100 Subject: [PATCH 1/2] build: Run tests using Grid --- .github/workflows/test.yml | 39 ++++++---- docker-compose.arm.yml | 61 +++++++++++++++ docker-compose.intel.yml | 77 +++++++++++++++++++ docker/Dockerfile | 14 ++++ docker/webserver.py | 8 ++ pyproject.toml | 1 - src/pytest_selenium/drivers/browserstack.py | 2 +- .../drivers/crossbrowsertesting.py | 4 +- src/pytest_selenium/drivers/remote.py | 7 ++ src/pytest_selenium/drivers/saucelabs.py | 4 +- src/pytest_selenium/drivers/testingbot.py | 4 +- src/pytest_selenium/pytest_selenium.py | 2 +- start | 11 +++ stop | 11 +++ testing/conftest.py | 27 +++++-- testing/test_chrome.py | 28 +++++-- testing/test_destructive.py | 25 +++--- testing/test_driver.py | 10 ++- testing/test_driver_log.py | 12 +-- testing/test_edge.py | 21 ++--- testing/test_firefox.py | 14 ++-- testing/test_report.py | 56 +++++++------- testing/test_webdriver.py | 2 +- tox.ini | 2 +- 24 files changed, 335 insertions(+), 107 deletions(-) create mode 100644 docker-compose.arm.yml create mode 100644 docker-compose.intel.yml create mode 100644 docker/Dockerfile create mode 100644 docker/webserver.py create mode 100755 start create mode 100755 stop diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 252af115..49ccd33a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -44,23 +44,30 @@ jobs: python -m pip install --upgrade pip pip install tox - - name: Setup Firefox + - name: Spin up Grid if: matrix.os == 'ubuntu-latest' - uses: browser-actions/setup-firefox@latest - with: - firefox-version: latest - - - name: Setup Geckodriver - if: matrix.os == 'ubuntu-latest' - uses: browser-actions/setup-geckodriver@latest + run: ./start - - name: Setup Chrome - uses: browser-actions/setup-chrome@latest - with: - chrome-version: stable - - - name: Setup Chromedriver - uses: nanasess/setup-chromedriver@master + - name: Spin up Grid + if: matrix.os == 'windows-latest' + run: docker-compose -f docker-compose.intel.yml up -d +# - name: Setup Firefox +# if: matrix.os == 'ubuntu-latest' +# uses: browser-actions/setup-firefox@latest +# with: +# firefox-version: latest +# +# - name: Setup Geckodriver +# if: matrix.os == 'ubuntu-latest' +# uses: browser-actions/setup-geckodriver@latest +# +# - name: Setup Chrome +# uses: browser-actions/setup-chrome@latest +# with: +# chrome-version: stable +# +# - name: Setup Chromedriver +# uses: nanasess/setup-chromedriver@master - name: Cache tox environments uses: actions/cache@v3 @@ -70,7 +77,7 @@ jobs: - name: Test if: matrix.os == 'ubuntu-latest' - run: tox -e ${{ matrix.tox_env }} + run: tox -e ${{ matrix.tox_env }} -- --html={envlogdir}/report.html --self-contained-html - name: Test (skip firefox on windows) if: matrix.os == 'windows-latest' diff --git a/docker-compose.arm.yml b/docker-compose.arm.yml new file mode 100644 index 00000000..cdc608b9 --- /dev/null +++ b/docker-compose.arm.yml @@ -0,0 +1,61 @@ +# To execute this docker-compose yml file use `docker-compose -f docker-compose.intel.yml up` +# Add the `-d` flag at the end for detached execution +# To stop the execution, hit Ctrl+C, and then `docker-compose -f docker-compose-v3.yml down` +version: "3" + +services: + + chromium: + image: seleniarm/node-chromium:latest + container_name: selenium-chromium + shm_size: 2gb + ports: + - "7901:7900" + depends_on: + - selenium-hub + environment: + - SE_EVENT_BUS_HOST=selenium-hub + - SE_EVENT_BUS_PUBLISH_PORT=4442 + - SE_EVENT_BUS_SUBSCRIBE_PORT=4443 + networks: + - grid + + firefox: + image: seleniarm/node-firefox:latest + container_name: selenium-firefox + shm_size: 2gb + ports: + - "7903:7900" + depends_on: + - selenium-hub + environment: + - SE_EVENT_BUS_HOST=selenium-hub + - SE_EVENT_BUS_PUBLISH_PORT=4442 + - SE_EVENT_BUS_SUBSCRIBE_PORT=4443 + networks: + - grid + + selenium-hub: + image: seleniarm/hub:latest + container_name: selenium-hub + ports: + - "4442:4442" + - "4443:4443" + - "4444:4444" + networks: + - grid + + webserver: + container_name: webserver + build: + context: docker/ + environment: + - PYTHONDONTWRITEBYTECODE=1 + networks: + - grid + depends_on: + - firefox + - chromium + +networks: + grid: diff --git a/docker-compose.intel.yml b/docker-compose.intel.yml new file mode 100644 index 00000000..b25c570a --- /dev/null +++ b/docker-compose.intel.yml @@ -0,0 +1,77 @@ +# To execute this docker-compose yml file use `docker-compose -f docker-compose.intel.yml up` +# Add the `-d` flag at the end for detached execution +# To stop the execution, hit Ctrl+C, and then `docker-compose -f docker-compose-v3.yml down` +version: "3" + +services: + + chrome: + image: selenium/node-chrome:latest + container_name: selenium-chrome + shm_size: 2gb + ports: + - "5901:5900" + depends_on: + - selenium-hub + environment: + - SE_EVENT_BUS_HOST=selenium-hub + - SE_EVENT_BUS_PUBLISH_PORT=4442 + - SE_EVENT_BUS_SUBSCRIBE_PORT=4443 + networks: + - grid + + edge: + image: selenium/node-edge:latest + container_name: selenium-edge + shm_size: 2gb + ports: + - "5902:5900" + depends_on: + - selenium-hub + environment: + - SE_EVENT_BUS_HOST=selenium-hub + - SE_EVENT_BUS_PUBLISH_PORT=4442 + - SE_EVENT_BUS_SUBSCRIBE_PORT=4443 + networks: + - grid + + firefox: + image: selenium/node-firefox:latest + container_name: selenium-firefox + shm_size: 2gb + ports: + - "5903:5900" + depends_on: + - selenium-hub + environment: + - SE_EVENT_BUS_HOST=selenium-hub + - SE_EVENT_BUS_PUBLISH_PORT=4442 + - SE_EVENT_BUS_SUBSCRIBE_PORT=4443 + networks: + - grid + + selenium-hub: + image: selenium/hub:latest + container_name: selenium-hub + ports: + - "4442:4442" + - "4443:4443" + - "4444:4444" + networks: + - grid + + webserver: + container_name: webserver + build: + context: docker/ + environment: + - PYTHONDONTWRITEBYTECODE=1 + networks: + - grid + depends_on: + - firefox + - chrome + - edge + +networks: + grid: diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 00000000..ae617a74 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,14 @@ +FROM python:3.10-slim-buster + +WORKDIR /usr/src/app + +ENV FLASK_APP=webserver.py +ENV FLASK_RUN_HOST=0.0.0.0 +ENV FLASK_RUN_PORT=80 + +RUN python -m pip install --upgrade pip && \ + pip install flask + +COPY webserver.py . + +CMD ["flask", "run"] diff --git a/docker/webserver.py b/docker/webserver.py new file mode 100644 index 00000000..c34a6f46 --- /dev/null +++ b/docker/webserver.py @@ -0,0 +1,8 @@ +from flask import Flask + +app = Flask(__name__) + + +@app.route("/") +def home(): + return """

Success!

Link

Ё

""" diff --git a/pyproject.toml b/pyproject.toml index 37d21e7a..04a11799 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,7 +61,6 @@ black = ">=22.1.0" flake8 = ">=4.0.1" tox = ">=3.24.5" pre-commit = ">=2.17.0" -pytest-localserver = ">=0.5.0" pytest-xdist = ">=2.4.0" pytest-mock = ">=3.6.1" diff --git a/src/pytest_selenium/drivers/browserstack.py b/src/pytest_selenium/drivers/browserstack.py index 123e0172..df03d0af 100644 --- a/src/pytest_selenium/drivers/browserstack.py +++ b/src/pytest_selenium/drivers/browserstack.py @@ -48,7 +48,7 @@ def job_access(self): return field -@pytest.mark.optionalhook +@pytest.hookimpl(optionalhook=True) def pytest_selenium_runtest_makereport(item, report, summary, extra): provider = BrowserStack() if not provider.uses_driver(item.config.getoption("driver")): diff --git a/src/pytest_selenium/drivers/crossbrowsertesting.py b/src/pytest_selenium/drivers/crossbrowsertesting.py index 8d933ca9..9c9683c0 100644 --- a/src/pytest_selenium/drivers/crossbrowsertesting.py +++ b/src/pytest_selenium/drivers/crossbrowsertesting.py @@ -33,7 +33,7 @@ def key(self): ) -@pytest.mark.optionalhook +@pytest.hookimpl(optionalhook=True) def pytest_selenium_capture_debug(item, report, extra): provider = CrossBrowserTesting() if not provider.uses_driver(item.config.getoption("driver")): @@ -57,7 +57,7 @@ def pytest_selenium_capture_debug(item, report, extra): extra.append(pytest_html.extras.html(_video_html(videos[0]))) -@pytest.mark.optionalhook +@pytest.hookimpl(optionalhook=True) def pytest_selenium_runtest_makereport(item, report, summary, extra): provider = CrossBrowserTesting() if not provider.uses_driver(item.config.getoption("driver")): diff --git a/src/pytest_selenium/drivers/remote.py b/src/pytest_selenium/drivers/remote.py index c597dedb..6801ccbb 100644 --- a/src/pytest_selenium/drivers/remote.py +++ b/src/pytest_selenium/drivers/remote.py @@ -4,6 +4,8 @@ import os +# from selenium.webdriver.chrome.options import Options + HOST = os.environ.get("SELENIUM_HOST", "localhost") PORT = os.environ.get("SELENIUM_PORT", 4444) @@ -12,8 +14,13 @@ def driver_kwargs(capabilities, host, port, **kwargs): host = host if host.startswith("http") else f"http://{host}" executor = f"{host}:{port}/wd/hub" + # options = Options() + # options.add_argument("--log-path=foo.log") + # print(options.to_capabilities()) + kwargs = { "command_executor": executor, "desired_capabilities": capabilities, + # "options": options, } return kwargs diff --git a/src/pytest_selenium/drivers/saucelabs.py b/src/pytest_selenium/drivers/saucelabs.py index d7ba6d46..dff8abe6 100644 --- a/src/pytest_selenium/drivers/saucelabs.py +++ b/src/pytest_selenium/drivers/saucelabs.py @@ -52,7 +52,7 @@ def uses_driver(self, driver): return driver.lower() == self.name.lower() -@pytest.mark.optionalhook +@pytest.hookimpl(optionalhook=True) def pytest_selenium_capture_debug(item, report, extra): provider = SauceLabs(item.config.getini("saucelabs_data_center")) if not provider.uses_driver(item.config.getoption("driver")): @@ -62,7 +62,7 @@ def pytest_selenium_capture_debug(item, report, extra): extra.append(pytest_html.extras.html(_video_html(item._driver.session_id))) -@pytest.mark.optionalhook +@pytest.hookimpl(optionalhook=True) def pytest_selenium_runtest_makereport(item, report, summary, extra): provider = SauceLabs(item.config.getini("saucelabs_data_center")) if not provider.uses_driver(item.config.getoption("driver")): diff --git a/src/pytest_selenium/drivers/testingbot.py b/src/pytest_selenium/drivers/testingbot.py index 66f27f0a..b9fa89f9 100644 --- a/src/pytest_selenium/drivers/testingbot.py +++ b/src/pytest_selenium/drivers/testingbot.py @@ -42,7 +42,7 @@ def secret(self): return self.get_credential("secret", ["TESTINGBOT_SECRET", "TESTINGBOT_PSW"]) -@pytest.mark.optionalhook +@pytest.hookimpl(optionalhook=True) def pytest_selenium_capture_debug(item, report, extra): provider = TestingBot() if not provider.uses_driver(item.config.getoption("driver")): @@ -56,7 +56,7 @@ def pytest_selenium_capture_debug(item, report, extra): extra.append(pytest_html.extras.html(_video_html(auth_url, session_id))) -@pytest.mark.optionalhook +@pytest.hookimpl(optionalhook=True) def pytest_selenium_runtest_makereport(item, report, summary, extra): provider = TestingBot() if not provider.uses_driver(item.config.getoption("driver")): diff --git a/src/pytest_selenium/pytest_selenium.py b/src/pytest_selenium/pytest_selenium.py index d4568a46..6089e64e 100644 --- a/src/pytest_selenium/pytest_selenium.py +++ b/src/pytest_selenium/pytest_selenium.py @@ -253,7 +253,7 @@ def pytest_report_header(config, startdir): return "driver: {0}".format(driver) -@pytest.mark.hookwrapper +@pytest.hookimpl(hookwrapper=True) def pytest_runtest_makereport(item, call): outcome = yield report = outcome.get_result() diff --git a/start b/start new file mode 100755 index 00000000..ab71f7cc --- /dev/null +++ b/start @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if [[ $(uname -m) == "arm64" ]]; then + arch="arm" +else + arch="intel" +fi + +docker-compose -f "docker-compose.${arch}.yml" up -d diff --git a/stop b/stop new file mode 100755 index 00000000..302275a8 --- /dev/null +++ b/stop @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if [[ $(uname -m) == "arm64" ]]; then + arch="arm" +else + arch="intel" +fi + +docker-compose -f "docker-compose.${arch}.yml" down diff --git a/testing/conftest.py b/testing/conftest.py index 58acf864..4c5c43cb 100644 --- a/testing/conftest.py +++ b/testing/conftest.py @@ -9,13 +9,13 @@ pytest_plugins = "pytester" -def base_url(httpserver): - return httpserver.url +def base_url(): + return "http://webserver" @pytest.fixture -def httpserver_base_url(httpserver): - return "--base-url={0}".format(base_url(httpserver)) +def httpserver_base_url(): + return "--base-url={0}".format(base_url()) @pytest.fixture(autouse=True) @@ -59,14 +59,28 @@ def chrome_options(chrome_options): def runpytestqa(*args, **kwargs): return testdir.runpytest( - httpserver_base_url, "--driver", "Firefox", *args, **kwargs + httpserver_base_url, + "--driver", + "remote", + "--capability", + "browserName", + "firefox", + *args, + **kwargs, ) testdir.runpytestqa = runpytestqa def inline_runqa(*args, **kwargs): return testdir.inline_run( - httpserver_base_url, "--driver", "Firefox", *args, **kwargs + httpserver_base_url, + "--driver", + "remote", + "--capability", + "browserName", + "firefox", + *args, + **kwargs, ) testdir.inline_runqa = inline_runqa @@ -74,6 +88,7 @@ def inline_runqa(*args, **kwargs): def quick_qa(*args, **kwargs): reprec = inline_runqa(*args) outcomes = reprec.listoutcomes() + print(f"outcomes: {outcomes}") names = ("passed", "skipped", "failed") for name, val in zip(names, outcomes): wantlen = kwargs.get(name) diff --git a/testing/test_chrome.py b/testing/test_chrome.py index f65afd1e..1d94cd4d 100644 --- a/testing/test_chrome.py +++ b/testing/test_chrome.py @@ -10,8 +10,7 @@ @pytest.mark.chrome -def test_launch(testdir, httpserver): - httpserver.serve_content(content="

Success!

") +def test_launch(testdir): file_test = testdir.makepyfile( """ import pytest @@ -20,7 +19,15 @@ def test_pass(webtext): assert webtext == u'Success!' """ ) - testdir.quick_qa("--driver", "Chrome", file_test, passed=1) + testdir.quick_qa( + "--driver", + "Remote", + "--capability", + "browserName", + "chrome", + file_test, + passed=1, + ) @pytest.mark.chrome @@ -37,13 +44,16 @@ def chrome_options(chrome_options): def test_pass(selenium): pass """ ) - reprec = testdir.inline_run("--driver", "Chrome") + reprec = testdir.inline_run( + "--driver", "Remote", "--capability", "browserName", "chrome" + ) passed, skipped, failed = reprec.listoutcomes() assert len(failed) == 1 out = failed[0].longrepr.reprcrash.message assert "no chrome binary at /foo/bar" in out +@pytest.mark.xfail(reason="Remote driver currently doesn't support logs") @pytest.mark.chrome def test_args(testdir): file_test = testdir.makepyfile( @@ -61,5 +71,13 @@ def driver_args(): def test_pass(selenium): pass """ ) - testdir.quick_qa("--driver", "Chrome", file_test, passed=1) + testdir.quick_qa( + "--driver", + "Remote", + "--capability", + "browserName", + "chrome", + file_test, + passed=1, + ) assert os.path.exists(str(testdir.tmpdir.join("foo.log"))) diff --git a/testing/test_destructive.py b/testing/test_destructive.py index ae230340..b6778c13 100644 --- a/testing/test_destructive.py +++ b/testing/test_destructive.py @@ -12,31 +12,30 @@ def test_skip_destructive_by_default(testdir): testdir.quick_qa(file_test, passed=0, failed=0, skipped=1) -def test_warn_when_url_is_sensitive(testdir, httpserver, monkeypatch, capsys): - monkeypatch.setenv("SENSITIVE_URL", r"127\.0\.0\.1") +def test_warn_when_url_is_sensitive(testdir, monkeypatch, capsys): + monkeypatch.setenv("SENSITIVE_URL", r"webserver") file_test = testdir.makepyfile("def test_pass(): pass") testdir.quick_qa(file_test, "--verbose", passed=0, failed=0, skipped=1) out, err = capsys.readouterr() - msg = "*** WARNING: sensitive url matches {} ***".format(httpserver.url) + msg = "*** WARNING: sensitive url matches http://webserver ***" assert msg in out -def test_skip_destructive_when_sensitive_command_line(testdir, httpserver): +def test_skip_destructive_when_sensitive_command_line(testdir): file_test = testdir.makepyfile("def test_pass(): pass") - print(httpserver.url) testdir.quick_qa( - "--sensitive-url", r"127\.0\.0\.1", file_test, passed=0, failed=0, skipped=1 + "--sensitive-url", "webserver", file_test, passed=0, failed=0, skipped=1 ) -def test_skip_destructive_when_sensitive_config_file(testdir, httpserver): - testdir.makefile(".ini", pytest="[pytest]\nsensitive_url=127\\.0\\.0\\.1") +def test_skip_destructive_when_sensitive_config_file(testdir): + testdir.makefile(".ini", pytest="[pytest]\nsensitive_url=webserver") file_test = testdir.makepyfile("def test_pass(): pass") testdir.quick_qa(file_test, passed=0, failed=0, skipped=1) -def test_skip_destructive_when_sensitive_env(testdir, httpserver, monkeypatch): - monkeypatch.setenv("SENSITIVE_URL", r"127\.0\.0\.1") +def test_skip_destructive_when_sensitive_env(testdir, monkeypatch): + monkeypatch.setenv("SENSITIVE_URL", "webserver") file_test = testdir.makepyfile("def test_pass(): pass") testdir.quick_qa(file_test, passed=0, failed=0, skipped=1) @@ -52,18 +51,18 @@ def test_pass(): pass testdir.quick_qa(file_test, passed=1) -def test_run_destructive_when_not_sensitive_command_line(testdir, httpserver): +def test_run_destructive_when_not_sensitive_command_line(testdir): file_test = testdir.makepyfile("def test_pass(): pass") testdir.quick_qa("--sensitive-url", "foo", file_test, passed=1) -def test_run_destructive_when_not_sensitive_config_file(testdir, httpserver): +def test_run_destructive_when_not_sensitive_config_file(testdir): testdir.makefile(".ini", pytest="[pytest]\nsensitive_url=foo") file_test = testdir.makepyfile("def test_pass(): pass") testdir.quick_qa(file_test, passed=1, failed=0, skipped=0) -def test_run_destructive_when_not_sensitive_env(testdir, httpserver, monkeypatch): +def test_run_destructive_when_not_sensitive_env(testdir, monkeypatch): monkeypatch.setenv("SENSITIVE_URL", "foo") file_test = testdir.makepyfile("def test_pass(): pass") testdir.quick_qa(file_test, passed=1, failed=0, skipped=0) diff --git a/testing/test_driver.py b/testing/test_driver.py index 07e01465..ca456b2d 100644 --- a/testing/test_driver.py +++ b/testing/test_driver.py @@ -203,6 +203,7 @@ def test_provider_naming(name): assert provider.name == name +@pytest.mark.xfail(reason="Remote driver currently doesn't support logs") def test_service_log_path(testdir): file_test = testdir.makepyfile( """ @@ -212,9 +213,10 @@ def test_pass(driver_kwargs): assert driver_kwargs['service_log_path'] is not None """ ) - testdir.quick_qa("--driver", "Firefox", file_test, passed=1) + testdir.quick_qa(file_test, passed=1) +@pytest.mark.xfail(reason="Remote driver currently doesn't support logs") def test_no_service_log_path(testdir): file_test = testdir.makepyfile( """ @@ -228,7 +230,7 @@ def test_pass(driver_kwargs): assert driver_kwargs['service_log_path'] is None """ ) - testdir.quick_qa("--driver", "Firefox", file_test, passed=1) + testdir.quick_qa(file_test, passed=1) def test_driver_retry_pass(testdir, mocker): @@ -248,7 +250,7 @@ def test_pass(driver): """ ) - testdir.quick_qa("--driver", "Firefox", file_test, passed=1) + testdir.quick_qa(file_test, passed=1) assert mock_retrying.spy_return.statistics["attempt_number"] == 1 @@ -296,4 +298,4 @@ def test_xdist(driver): pass """ ) - testdir.quick_qa("--driver", "firefox", "-n", "2", file_test, passed=1) + testdir.quick_qa("-n", "2", file_test, passed=1) diff --git a/testing/test_driver_log.py b/testing/test_driver_log.py index d2ac77df..883a2c51 100644 --- a/testing/test_driver_log.py +++ b/testing/test_driver_log.py @@ -12,8 +12,8 @@ LOG_REGEX = 'Driver Log' -def test_driver_log(testdir, httpserver): - httpserver.serve_content(content="

Success!

") +@pytest.mark.xfail(reason="Remote driver currently doesn't support logs") +def test_driver_log(testdir): testdir.makepyfile( """ import pytest @@ -26,13 +26,14 @@ def test_driver_log(webtext): testdir.runpytestqa("--html", path) with open(str(path)) as f: html = f.read() + assert re.search(LOG_REGEX, html) is not None log_path = testdir.tmpdir.dirpath("basetemp", "test_driver_log0", "driver.log") assert os.path.exists(str(log_path)) -def test_driver_log_fixture(testdir, httpserver): - httpserver.serve_content(content="

Success!

") +@pytest.mark.xfail(reason="Remote driver currently doesn't support logs") +def test_driver_log_fixture(testdir): file_test = testdir.makepyfile( """ import pytest @@ -49,8 +50,7 @@ def test_pass(webtext): assert os.path.exists(str(testdir.tmpdir.join("foo.log"))) -def test_no_driver_log(testdir, httpserver): - httpserver.serve_content(content="

Success!

") +def test_no_driver_log(testdir): testdir.makepyfile( """ import pytest diff --git a/testing/test_edge.py b/testing/test_edge.py index 18a8fb2a..002edae5 100644 --- a/testing/test_edge.py +++ b/testing/test_edge.py @@ -3,16 +3,16 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. import pytest -import sys + +# import sys pytestmark = pytest.mark.nondestructive -@pytest.mark.skipif(sys.platform != "win32", reason="Edge only runs on Windows") +# @pytest.mark.skipif(sys.platform != "win32", reason="Edge only runs on Windows") @pytest.mark.edge -def test_launch_legacy(testdir, httpserver): - httpserver.serve_content(content="

Success!

") +def test_launch_legacy(testdir): file_test = testdir.makepyfile( """ import pytest @@ -21,14 +21,15 @@ def test_pass(webtext): assert webtext == u'Success!' """ ) - testdir.quick_qa("--driver", "Edge", file_test, passed=1) + testdir.quick_qa( + "--driver", "remote", "--capability", "browserName", "edge", file_test, passed=1 + ) -@pytest.mark.skipif(sys.platform != "win32", reason="Edge only runs on Windows") +# @pytest.mark.skipif(sys.platform != "win32", reason="Edge only runs on Windows") @pytest.mark.edge @pytest.mark.parametrize("use_chromium", [True, False], ids=["chromium", "legacy"]) -def test_launch(use_chromium, testdir, httpserver): - httpserver.serve_content(content="

Success!

") +def test_launch(use_chromium, testdir): file_test = testdir.makepyfile( """ import pytest @@ -45,4 +46,6 @@ def edge_options(edge_options): use_chromium ) ) - testdir.quick_qa("--driver", "Edge", file_test, passed=1) + testdir.quick_qa( + "--driver", "remote", "--capability", "browserName", "edge", file_test, passed=1 + ) diff --git a/testing/test_firefox.py b/testing/test_firefox.py index 68476da9..d11b6817 100644 --- a/testing/test_firefox.py +++ b/testing/test_firefox.py @@ -7,8 +7,7 @@ pytestmark = [pytest.mark.nondestructive, pytest.mark.firefox] -def test_launch(testdir, httpserver): - httpserver.serve_content(content="

Success!

") +def test_launch(testdir): file_test = testdir.makepyfile( """ import pytest @@ -20,8 +19,7 @@ def test_pass(webtext): testdir.quick_qa(file_test, passed=1) -def test_launch_case_insensitive(testdir, httpserver): - httpserver.serve_content(content="

Success!

") +def test_launch_case_insensitive(testdir): file_test = testdir.makepyfile( """ import pytest @@ -30,16 +28,15 @@ def test_pass(webtext): assert webtext == u'Success!' """ ) - testdir.quick_qa("--driver", "firefox", file_test, passed=1) + testdir.quick_qa(file_test, passed=1) -def test_profile(testdir, httpserver): +def test_profile(testdir): """Test that specified profile is used when starting Firefox. The profile changes the colors in the browser, which are then reflected when calling value_of_css_property. """ - httpserver.serve_content(content='

Success!

Link') file_test = testdir.makepyfile( """ import pytest @@ -96,9 +93,8 @@ def test_extension(selenium): testdir.quick_qa("--firefox-extension", extension, file_test, passed=1) -def test_preferences_marker(testdir, httpserver): +def test_preferences_marker(testdir): """Test that preferences can be specified using the marker.""" - httpserver.serve_content(content='

Success!

Link') file_test = testdir.makepyfile( """ import pytest diff --git a/testing/test_report.py b/testing/test_report.py index 7630bd0a..0e489b47 100644 --- a/testing/test_report.py +++ b/testing/test_report.py @@ -38,8 +38,8 @@ def test_fail(webtext): @pytest.mark.parametrize("when", ["always", "failure", "never"]) -def test_capture_debug_env(testdir, httpserver, monkeypatch, when): - httpserver.serve_content(content="

Success!

Ё

") +def test_capture_debug_env(testdir, monkeypatch, when): + # httpserver.serve_content(content="

Success!

Ё

") monkeypatch.setenv("SELENIUM_CAPTURE_DEBUG", when) testdir.makepyfile( """ @@ -53,22 +53,22 @@ def test_capture_debug(webtext): ) result, html = run(testdir) if when in ["always", "failure"]: - assert URL_LINK.format(httpserver.url) in html + assert URL_LINK.format("http://webserver") in html assert re.search(SCREENSHOT_LINK_REGEX, html) is not None assert re.search(SCREENSHOT_REGEX, html) is not None - assert re.search(LOGS_REGEX, html) is not None + # assert re.search(LOGS_REGEX, html) is not None assert re.search(HTML_REGEX, html) is not None else: - assert URL_LINK.format(httpserver.url) not in html + assert URL_LINK.format("http://webserver") not in html assert re.search(SCREENSHOT_LINK_REGEX, html) is None assert re.search(SCREENSHOT_REGEX, html) is None - assert re.search(LOGS_REGEX, html) is None + # assert re.search(LOGS_REGEX, html) is None assert re.search(HTML_REGEX, html) is None @pytest.mark.parametrize("when", ["always", "failure", "never"]) -def test_capture_debug_config(testdir, httpserver, when): - httpserver.serve_content(content="

Success!

Ё

") +def test_capture_debug_config(testdir, when): + # httpserver.serve_content(content="

Success!

Ё

") testdir.makefile( ".ini", pytest=""" @@ -90,30 +90,30 @@ def test_capture_debug(webtext): ) result, html = run(testdir) if when in ["always", "failure"]: - assert URL_LINK.format(httpserver.url) in html + assert URL_LINK.format("http://webserver") in html assert re.search(SCREENSHOT_LINK_REGEX, html) is not None assert re.search(SCREENSHOT_REGEX, html) is not None - assert re.search(LOGS_REGEX, html) is not None + # assert re.search(LOGS_REGEX, html) is not None assert re.search(HTML_REGEX, html) is not None else: - assert URL_LINK.format(httpserver.url) not in html + assert URL_LINK.format("http://webserver") not in html assert re.search(SCREENSHOT_LINK_REGEX, html) is None assert re.search(SCREENSHOT_REGEX, html) is None - assert re.search(LOGS_REGEX, html) is None + # assert re.search(LOGS_REGEX, html) is None assert re.search(HTML_REGEX, html) is None @pytest.mark.parametrize("exclude", ["url", "screenshot", "html", "logs"]) -def test_exclude_debug_env(testdir, httpserver, monkeypatch, exclude): - httpserver.serve_content(content="

Success!

Ё

") +def test_exclude_debug_env(testdir, monkeypatch, exclude): + # httpserver.serve_content(content="

Success!

Ё

") monkeypatch.setenv("SELENIUM_EXCLUDE_DEBUG", exclude) result, html = run(testdir) assert result.ret if exclude == "url": - assert URL_LINK.format(httpserver.url) not in html + assert URL_LINK.format("http://webserver") not in html else: - assert URL_LINK.format(httpserver.url) in html + assert URL_LINK.format("http://webserver") in html if exclude == "screenshot": assert re.search(SCREENSHOT_LINK_REGEX, html) is None @@ -122,10 +122,10 @@ def test_exclude_debug_env(testdir, httpserver, monkeypatch, exclude): assert re.search(SCREENSHOT_LINK_REGEX, html) is not None assert re.search(SCREENSHOT_REGEX, html) is not None - if exclude == "logs": - assert re.search(LOGS_REGEX, html) is None - else: - assert re.search(LOGS_REGEX, html) is not None + # if exclude == "logs": + # assert re.search(LOGS_REGEX, html) is None + # else: + # assert re.search(LOGS_REGEX, html) is not None if exclude == "html": assert re.search(HTML_REGEX, html) is None @@ -134,8 +134,8 @@ def test_exclude_debug_env(testdir, httpserver, monkeypatch, exclude): @pytest.mark.parametrize("exclude", ["url", "screenshot", "html", "logs"]) -def test_exclude_debug_config(testdir, httpserver, exclude): - httpserver.serve_content(content="

Success!

Ё

") +def test_exclude_debug_config(testdir, exclude): + # httpserver.serve_content(content="

Success!

Ё

") testdir.makefile( ".ini", pytest=""" @@ -149,9 +149,9 @@ def test_exclude_debug_config(testdir, httpserver, exclude): assert result.ret if exclude == "url": - assert URL_LINK.format(httpserver.url) not in html + assert URL_LINK.format("http://webserver") not in html else: - assert URL_LINK.format(httpserver.url) in html + assert URL_LINK.format("http://webserver") in html if exclude == "screenshot": assert re.search(SCREENSHOT_LINK_REGEX, html) is None @@ -160,10 +160,10 @@ def test_exclude_debug_config(testdir, httpserver, exclude): assert re.search(SCREENSHOT_LINK_REGEX, html) is not None assert re.search(SCREENSHOT_REGEX, html) is not None - if exclude == "logs": - assert re.search(LOGS_REGEX, html) is None - else: - assert re.search(LOGS_REGEX, html) is not None + # if exclude == "logs": + # assert re.search(LOGS_REGEX, html) is None + # else: + # assert re.search(LOGS_REGEX, html) is not None if exclude == "html": assert re.search(HTML_REGEX, html) is None diff --git a/testing/test_webdriver.py b/testing/test_webdriver.py index 16dbed50..f2f483a1 100644 --- a/testing/test_webdriver.py +++ b/testing/test_webdriver.py @@ -8,7 +8,7 @@ pytestmark = pytest.mark.nondestructive -def test_event_listening_webdriver(testdir, httpserver): +def test_event_listening_webdriver(testdir): file_test = testdir.makepyfile( """ import pytest diff --git a/tox.ini b/tox.ini index 963138e8..73920c4d 100644 --- a/tox.ini +++ b/tox.ini @@ -16,7 +16,7 @@ deps = pytest-localserver pytest-xdist pytest-mock -commands = pytest -n auto -v -r a --color=yes --strict-config --strict-markers --html={envlogdir}/report.html --self-contained-html {posargs} +commands = pytest -n auto -v -r a --color=yes --strict-config --strict-markers {posargs} [testenv:docs] basepython = python3 From cc7a7deb055096e731c3069edd3741b165403e78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jim=20Br=C3=A4nnlund?= Date: Sat, 27 May 2023 23:26:52 +0200 Subject: [PATCH 2/2] tbd --- .github/workflows/test.yml | 3 - geckodriver.log | 0 save.py | 567 +++++++++++++++++++++ src/pytest_selenium/drivers/firefox_new.py | 66 +++ 4 files changed, 633 insertions(+), 3 deletions(-) create mode 100644 geckodriver.log create mode 100644 save.py create mode 100644 src/pytest_selenium/drivers/firefox_new.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 49ccd33a..2c224059 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -48,9 +48,6 @@ jobs: if: matrix.os == 'ubuntu-latest' run: ./start - - name: Spin up Grid - if: matrix.os == 'windows-latest' - run: docker-compose -f docker-compose.intel.yml up -d # - name: Setup Firefox # if: matrix.os == 'ubuntu-latest' # uses: browser-actions/setup-firefox@latest diff --git a/geckodriver.log b/geckodriver.log new file mode 100644 index 00000000..e69de29b diff --git a/save.py b/save.py new file mode 100644 index 00000000..ffd84f1a --- /dev/null +++ b/save.py @@ -0,0 +1,567 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import argparse + +# import copy +from datetime import datetime +import os +import io +import logging + +# import importlib + +import pytest +from selenium import webdriver + +# from selenium.webdriver.common.options import ArgOptions +from selenium.webdriver.common.desired_capabilities import DesiredCapabilities +from selenium.webdriver.support.event_firing_webdriver import EventFiringWebDriver +from tenacity import Retrying, stop_after_attempt, wait_exponential + +from .utils import CaseInsensitiveDict +from . import drivers + + +LOGGER = logging.getLogger(__name__) + +SUPPORTED_DRIVERS = CaseInsensitiveDict( + { + "BrowserStack": webdriver.Remote, + "CrossBrowserTesting": webdriver.Remote, + "Chrome": webdriver.Chrome, + "Edge": webdriver.Edge, + "Firefox": webdriver.Firefox, + "IE": webdriver.Ie, + "Remote": webdriver.Remote, + "Safari": webdriver.Safari, + "SauceLabs": webdriver.Remote, + "TestingBot": webdriver.Remote, + } +) + +OPTIONS = { + "chrome": webdriver.ChromeOptions, + "edge": webdriver.EdgeOptions, + "MicrosoftEdge": webdriver.EdgeOptions, + "firefox": webdriver.FirefoxOptions, + "ie": webdriver.IeOptions, + "internet explorer": webdriver.IeOptions, + "safari": webdriver.safari.options.Options, +} + +SERVICE = { + "chrome": webdriver.chrome.service.Service, + "edge": webdriver.edge.service.Service, + "MicrosoftEdge": webdriver.edge.service.Service, + "firefox": webdriver.firefox.service.Service, + "ie": webdriver.ie.service.Service, + "internet explorer": webdriver.ie.service.Service, + "safari": webdriver.safari.service.Service, +} + +try: + from appium import webdriver as appiumdriver + from appium.options.common import base + + SUPPORTED_DRIVERS["Appium"] = appiumdriver.Remote + OPTIONS["Appium"] = base.AppiumOptions +except ImportError: + pass # Appium is optional. + + +def _merge(a, b): + """merges b and a configurations. + Based on http://bit.ly/2uFUHgb + """ + for key in b: + if key in a: + if isinstance(a[key], dict) and isinstance(b[key], dict): + _merge(a[key], b[key], [] + [str(key)]) + elif a[key] == b[key]: + pass # same leaf value + elif isinstance(a[key], list): + if isinstance(b[key], list): + a[key].extend(b[key]) + else: + a[key].append(b[key]) + else: + # b wins + a[key] = b[key] + else: + a[key] = b[key] + return a + + +def pytest_addhooks(pluginmanager): + from . import hooks + + method = getattr(pluginmanager, "add_hookspecs", None) + if method is None: + method = pluginmanager.addhooks + method(hooks) + + +@pytest.fixture(scope="session") +def session_capabilities(pytestconfig): + """Returns combined capabilities from pytest-variables and command line""" + driver = pytestconfig.getoption("driver") + capabilities = getattr(DesiredCapabilities, driver.upper(), {}).copy() + capabilities.update(pytestconfig._capabilities) + browser = capabilities["browserName"] + options = OPTIONS[browser.lower()]() + + for name, value in capabilities.items(): + options.set_capability(name, value) + + pytestconfig._options = options + return options + + +# @pytest.fixture(scope="session") +# def session_capabilities(pytestconfig): +# """Returns combined capabilities from pytest-variables and command line""" +# capabilities = pytestconfig._capabilities +# driver = pytestconfig.getoption("driver") +# if driver.upper() == "REMOTE": +# +# module = importlib.import_module(SUPPORTED_DRIVERS[driver], "selenium.webdriver") +# try: +# options = module.options.Options() +# except AttributeError: +# # Remote driver doesn't have options +# browser = capabilities.get("browserName", "") +# module = importlib.import_module(f".{browser}", "selenium.webdriver") +# options = module.options.Options() +# +# for name, value in capabilities.items(): +# options.set_capability(name, value) +# +# return options + +# @pytest.fixture(scope="session") +# def session_capabilities(pytestconfig): +# """Returns combined capabilities from pytest-variables and command line""" +# driver = pytestconfig.getoption("driver").upper() +# capabilities = getattr(DesiredCapabilities, driver, {}).copy() +# if driver == "REMOTE": +# browser = capabilities.get("browserName", "").upper() +# capabilities.update(getattr(DesiredCapabilities, browser, {})) +# capabilities.update(pytestconfig._capabilities) +# return capabilities + + +@pytest.fixture +def options(pytestconfig): + return pytestconfig._options + + +@pytest.fixture +def service(driver_class, capabilities): + if driver_class != webdriver.Remote: + service = SERVICE[capabilities["browserName"]] + service = service( + driver_path, + ) + + +@pytest.fixture +def capabilities( + request, + # driver_class, + chrome_options, + firefox_options, + edge_options, + options, + session_capabilities, +): + """Returns combined capabilities""" + # capabilities = copy.deepcopy(session_capabilities) # make a copy + # if driver_class == webdriver.Remote: + # browser = capabilities.get("browserName", "").upper() + # key, options = (None, None) + # if browser == "CHROME": + # key = getattr(chrome_options, "KEY", "goog:chromeOptions") + # options = chrome_options.to_capabilities() + # if key not in options: + # key = "chromeOptions" + # elif browser == "FIREFOX": + # key = firefox_options.KEY + # options = firefox_options.to_capabilities() + # elif browser == "EDGE": + # key = getattr(edge_options, "KEY", None) + # options = edge_options.to_capabilities() + # if all([key, options]): + # capabilities[key] = + # _merge(capabilities.get(key, {}), options.get(key, {})) + for name, value in get_capabilities_from_markers(request.node).items(): + session_capabilities.set_capability(name, value) + # capabilities.update(get_capabilities_from_markers(request.node)) + return session_capabilities + + +def get_capabilities_from_markers(node): + capabilities = dict() + for level, mark in node.iter_markers_with_node("capabilities"): + LOGGER.debug( + "{0} marker <{1.name}> " + "contained kwargs <{1.kwargs}>".format(level.__class__.__name__, mark) + ) + capabilities.update(mark.kwargs) + LOGGER.info("Capabilities from markers: {}".format(capabilities)) + return capabilities + + +@pytest.fixture +def driver_args(): + """Return arguments to pass to the driver service""" + # TODO deprecated Service + return None + + +@pytest.fixture +def driver_kwargs( + request, + capabilities, + chrome_options, + # driver_args, + driver_class, + # driver_log, + # driver_path, + firefox_options, + edge_options, + service, + pytestconfig, +): + kwargs = {} + driver = getattr(drivers, pytestconfig.getoption("driver").lower()) + kwargs.update( + driver.driver_kwargs( + # capabilities=capabilities.to_capabilities(), + # chrome_options=chrome_options, + driver_args=driver_args, + # driver_log=driver_log, + # driver_path=driver_path, + # firefox_options=firefox_options, + # edge_options=edge_options, + options=options, + host=pytestconfig.getoption("selenium_host"), + port=pytestconfig.getoption("selenium_port"), + service_log_path=None, + service=service, + request=request, + test=".".join(split_class_and_test_names(request.node.nodeid)), + ) + ) + + pytestconfig._driver_log = driver_log + return kwargs + + +@pytest.fixture(scope="session") +def driver_class(request): + driver = request.config.getoption("driver") + if driver is None: + raise pytest.UsageError("--driver must be specified") + return SUPPORTED_DRIVERS[driver] + + +@pytest.fixture +def driver_log(tmpdir): + """Return path to driver log""" + # TODO deprecated Service + return str(tmpdir.join("driver.log")) + + +@pytest.fixture +def driver_path(request): + # TODO deprecated Service + return request.config.getoption("driver_path") + + +@pytest.fixture +def driver(request, driver_class, driver_kwargs): + """Returns a WebDriver instance based on options and capabilities""" + + retries = int(request.config.getini("max_driver_init_attempts")) + for retry in Retrying( + stop=stop_after_attempt(retries), wait=wait_exponential(), reraise=True + ): + with retry: + LOGGER.info( + f"Driver init, attempt {retry.retry_state.attempt_number}/{retries}" + ) + driver = driver_class(**driver_kwargs) + + event_listener = request.config.getoption("event_listener") + if event_listener is not None: + # Import the specified event listener and wrap the driver instance + mod_name, class_name = event_listener.rsplit(".", 1) + mod = __import__(mod_name, fromlist=[class_name]) + event_listener = getattr(mod, class_name) + if not isinstance(driver, EventFiringWebDriver): + driver = EventFiringWebDriver(driver, event_listener()) + + request.node._driver = driver + yield driver + driver.quit() + + +@pytest.fixture +def selenium(driver): + yield driver + + +@pytest.hookimpl(trylast=True) +def pytest_configure(config): + capabilities = config._variables.get("capabilities", {}) + capabilities.update({k: v for k, v in config.getoption("capabilities")}) + config.addinivalue_line( + "markers", + "capabilities(kwargs): add or change existing " + "capabilities. specify capabilities as keyword arguments, for example " + "capabilities(foo=" + "bar" + ")", + ) + if hasattr(config, "_metadata"): + config._metadata["Driver"] = config.getoption("driver") + config._metadata["Capabilities"] = capabilities + if all((config.option.selenium_host, config.option.selenium_port)): + config._metadata["Server"] = "{0}:{1}".format( + config.option.selenium_host, config.option.selenium_port + ) + config._capabilities = capabilities + + +def pytest_report_header(config, startdir): + driver = config.getoption("driver") + if driver is not None: + return "driver: {0}".format(driver) + + +@pytest.mark.hookwrapper +def pytest_runtest_makereport(item, call): + outcome = yield + report = outcome.get_result() + summary = [] + extra = getattr(report, "extra", []) + driver = getattr(item, "_driver", None) + xfail = hasattr(report, "wasxfail") + failure = (report.skipped and xfail) or (report.failed and not xfail) + when = item.config.getini("selenium_capture_debug").lower() + capture_debug = when == "always" or (when == "failure" and failure) + if capture_debug: + exclude = item.config.getini("selenium_exclude_debug").lower() + if "logs" not in exclude: + # gather logs that do not depend on a driver instance + _gather_driver_log(item, summary, extra) + if driver is not None: + # gather debug that depends on a driver instance + if "url" not in exclude: + _gather_url(item, report, driver, summary, extra) + if "screenshot" not in exclude: + _gather_screenshot(item, report, driver, summary, extra) + if "html" not in exclude: + _gather_html(item, report, driver, summary, extra) + if "logs" not in exclude: + _gather_logs(item, report, driver, summary, extra) + # gather debug from hook implementations + item.config.hook.pytest_selenium_capture_debug( + item=item, report=report, extra=extra + ) + if driver is not None: + # allow hook implementations to further modify the report + item.config.hook.pytest_selenium_runtest_makereport( + item=item, report=report, summary=summary, extra=extra + ) + if summary: + report.sections.append(("pytest-selenium", "\n".join(summary))) + report.extra = extra + + +def _gather_url(item, report, driver, summary, extra): + try: + url = driver.current_url + except Exception as e: + summary.append("WARNING: Failed to gather URL: {0}".format(e)) + return + pytest_html = item.config.pluginmanager.getplugin("html") + if pytest_html is not None: + # add url to the html report + extra.append(pytest_html.extras.url(url)) + summary.append("URL: {0}".format(url)) + + +def _gather_screenshot(item, report, driver, summary, extra): + try: + screenshot = driver.get_screenshot_as_base64() + except Exception as e: + summary.append("WARNING: Failed to gather screenshot: {0}".format(e)) + return + pytest_html = item.config.pluginmanager.getplugin("html") + if pytest_html is not None: + # add screenshot to the html report + extra.append(pytest_html.extras.image(screenshot, "Screenshot")) + + +def _gather_html(item, report, driver, summary, extra): + try: + html = driver.page_source + except Exception as e: + summary.append("WARNING: Failed to gather HTML: {0}".format(e)) + return + pytest_html = item.config.pluginmanager.getplugin("html") + if pytest_html is not None: + # add page source to the html report + extra.append(pytest_html.extras.text(html, "HTML")) + + +def _gather_logs(item, report, driver, summary, extra): + pytest_html = item.config.pluginmanager.getplugin("html") + try: + types = driver.log_types + except Exception as e: + # note that some drivers may not implement log types + summary.append("WARNING: Failed to gather log types: {0}".format(e)) + return + for name in types: + try: + log = driver.get_log(name) + except Exception as e: + summary.append("WARNING: Failed to gather {0} log: {1}".format(name, e)) + return + if pytest_html is not None: + extra.append( + pytest_html.extras.text(format_log(log), "%s Log" % name.title()) + ) + + +def _gather_driver_log(item, summary, extra): + pytest_html = item.config.pluginmanager.getplugin("html") + if ( + hasattr(item.config, "_driver_log") + and item.config._driver_log is not None + and os.path.exists(item.config._driver_log) + ): + if pytest_html is not None: + with io.open(item.config._driver_log, "r", encoding="utf8") as f: + extra.append(pytest_html.extras.text(f.read(), "Driver Log")) + summary.append("Driver log: {0}".format(item.config._driver_log)) + + +def format_log(log): + timestamp_format = "%Y-%m-%d %H:%M:%S.%f" + entries = [ + "{0} {1[level]} - {1[message]}".format( + datetime.utcfromtimestamp(entry["timestamp"] / 1000.0).strftime( + timestamp_format + ), + entry, + ).rstrip() + for entry in log + ] + log = "\n".join(entries) + return log + + +def split_class_and_test_names(nodeid): + """Returns the class and method name from the current test""" + names = nodeid.split("::") + names[0] = names[0].replace("/", ".") + names = [x.replace(".py", "") for x in names if x != "()"] + classnames = names[:-1] + classname = ".".join(classnames) + name = names[-1] + return classname, name + + +class DriverAction(argparse.Action): + def __call__(self, parser, namespace, values, option_string=None): + setattr(namespace, self.dest, values) + driver = getattr(drivers, values.lower()) + # set the default host and port if specified in the driver module + namespace.selenium_host = namespace.selenium_host or getattr( + driver, "HOST", None + ) + namespace.selenium_port = namespace.selenium_port or getattr( + driver, "PORT", None + ) + + +def pytest_addoption(parser): + _capture_choices = ("never", "failure", "always") + parser.addini( + "selenium_capture_debug", + help="when debug is captured {0}".format(_capture_choices), + default=os.getenv("SELENIUM_CAPTURE_DEBUG", "failure"), + ) + parser.addini( + "selenium_exclude_debug", + help="debug to exclude from capture", + default=os.getenv("SELENIUM_EXCLUDE_DEBUG"), + ) + + _auth_choices = ("none", "token", "hour", "day") + parser.addini( + "saucelabs_job_auth", + help="Authorization options for the Sauce Labs job: {0}".format(_auth_choices), + default=os.getenv("SAUCELABS_JOB_AUTH", "none"), + ) + + _data_center_choices = ("us-west-1", "us-east-1", "eu-central-1") + parser.addini( + "saucelabs_data_center", + help="Data center options for Sauce Labs connections: {0}".format( + _data_center_choices + ), + default="us-west-1", + ) + + parser.addini( + "max_driver_init_attempts", + help="Maximum number of driver initialization attempts", + default=3, + ) + group = parser.getgroup("selenium", "selenium") + group._addoption( + "--driver", + action=DriverAction, + choices=SUPPORTED_DRIVERS, + help="webdriver implementation.", + metavar="str", + ) + group._addoption( + "--driver-path", metavar="path", help="path to the driver executable." + ) + group._addoption( + "--capability", + action="append", + default=[], + dest="capabilities", + metavar=("key", "value"), + nargs=2, + help="additional capabilities.", + ) + group._addoption( + "--event-listener", + metavar="str", + help="selenium eventlistener class, e.g. " + "package.module.EventListenerClassName.", + ) + group._addoption( + "--selenium-host", + metavar="str", + help="host that the selenium server is listening on, " + "which will default to the cloud provider default " + "or localhost.", + ) + group._addoption( + "--selenium-port", + type=int, + metavar="num", + help="port that the selenium server is listening on, " + "which will default to the cloud provider default " + "or localhost.", + ) diff --git a/src/pytest_selenium/drivers/firefox_new.py b/src/pytest_selenium/drivers/firefox_new.py new file mode 100644 index 00000000..4a9a2c16 --- /dev/null +++ b/src/pytest_selenium/drivers/firefox_new.py @@ -0,0 +1,66 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +import logging + +import pytest + +from selenium.webdriver.firefox.options import Options + +LOGGER = logging.getLogger(__name__) + + +def pytest_configure(config): + config.addinivalue_line( + "markers", + "firefox_arguments(args): arguments to be passed to " + "Firefox. This marker will be ignored for other browsers. For " + "example: firefox_arguments('-foreground')", + ) + config.addinivalue_line( + "markers", + "firefox_preferences(dict): preferences to be passed to " + "Firefox. This marker will be ignored for other browsers. For " + "example: firefox_preferences({'browser.startup.homepage': " + "'https://pytest.org/'})", + ) + + +def driver_kwargs(capabilities, driver_log, driver_path, firefox_options, **kwargs): + kwargs = {"service_log_path": driver_log} + + if capabilities: + kwargs["capabilities"] = capabilities + if driver_path is not None: + kwargs["executable_path"] = driver_path + + kwargs["options"] = firefox_options + + return kwargs + + +@pytest.fixture +def firefox_options(request): + options = Options() + + for arg in get_arguments_from_markers(request.node): + options.add_argument(arg) + + for name, value in get_preferences_from_markers(request.node).items(): + options.set_preference(name, value) + + return options + + +def get_arguments_from_markers(node): + arguments = [] + for m in node.iter_markers("firefox_arguments"): + arguments.extend(m.args) + return arguments + + +def get_preferences_from_markers(node): + preferences = dict() + for mark in node.iter_markers("firefox_preferences"): + preferences.update(mark.args[0]) + return preferences