diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 252af11..2c22405 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -44,23 +44,27 @@ 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 - - - name: Setup Chrome - uses: browser-actions/setup-chrome@latest - with: - chrome-version: stable - - - name: Setup Chromedriver - uses: nanasess/setup-chromedriver@master + run: ./start + +# - 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 +74,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 0000000..cdc608b --- /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 0000000..b25c570 --- /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 0000000..ae617a7 --- /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 0000000..c34a6f4 --- /dev/null +++ b/docker/webserver.py @@ -0,0 +1,8 @@ +from flask import Flask + +app = Flask(__name__) + + +@app.route("/") +def home(): + return """
Ё
""" diff --git a/geckodriver.log b/geckodriver.log new file mode 100644 index 0000000..e69de29 diff --git a/pyproject.toml b/pyproject.toml index 37d21e7..04a1179 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/save.py b/save.py new file mode 100644 index 0000000..ffd84f1 --- /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/browserstack.py b/src/pytest_selenium/drivers/browserstack.py index 123e017..df03d0a 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 8d933ca..9c9683c 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/firefox_new.py b/src/pytest_selenium/drivers/firefox_new.py new file mode 100644 index 0000000..4a9a2c1 --- /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 diff --git a/src/pytest_selenium/drivers/remote.py b/src/pytest_selenium/drivers/remote.py index c597ded..6801ccb 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 d7ba6d4..dff8abe 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 66f27f0..b9fa89f 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 d4568a4..6089e64 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 0000000..ab71f7c --- /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 0000000..302275a --- /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 58acf86..4c5c43c 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 f65afd1..1d94cd4 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="Ё
") +def test_capture_debug_env(testdir, monkeypatch, when): + # httpserver.serve_content(content="Ё
") 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="Ё
") +def test_capture_debug_config(testdir, when): + # httpserver.serve_content(content="Ё
") 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="Ё
") +def test_exclude_debug_env(testdir, monkeypatch, exclude): + # httpserver.serve_content(content="Ё
") 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="Ё
") +def test_exclude_debug_config(testdir, exclude): + # httpserver.serve_content(content="Ё
") 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 16dbed5..f2f483a 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 963138e..73920c4 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