jc-selenium-helper 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. jc_selenium_helper-0.1.0/LICENSE +21 -0
  2. jc_selenium_helper-0.1.0/PKG-INFO +82 -0
  3. jc_selenium_helper-0.1.0/README.md +40 -0
  4. jc_selenium_helper-0.1.0/pyproject.toml +84 -0
  5. jc_selenium_helper-0.1.0/setup.cfg +4 -0
  6. jc_selenium_helper-0.1.0/src/jc_selenium_helper/__init__.py +7 -0
  7. jc_selenium_helper-0.1.0/src/jc_selenium_helper/axe.py +21 -0
  8. jc_selenium_helper-0.1.0/src/jc_selenium_helper/browser.py +190 -0
  9. jc_selenium_helper-0.1.0/src/jc_selenium_helper/colors.py +14 -0
  10. jc_selenium_helper-0.1.0/src/jc_selenium_helper/config.py +28 -0
  11. jc_selenium_helper-0.1.0/src/jc_selenium_helper/legacy.py +158 -0
  12. jc_selenium_helper-0.1.0/src/jc_selenium_helper/plugin.py +43 -0
  13. jc_selenium_helper-0.1.0/src/jc_selenium_helper/py.typed +0 -0
  14. jc_selenium_helper-0.1.0/src/jc_selenium_helper.egg-info/PKG-INFO +82 -0
  15. jc_selenium_helper-0.1.0/src/jc_selenium_helper.egg-info/SOURCES.txt +28 -0
  16. jc_selenium_helper-0.1.0/src/jc_selenium_helper.egg-info/dependency_links.txt +1 -0
  17. jc_selenium_helper-0.1.0/src/jc_selenium_helper.egg-info/entry_points.txt +2 -0
  18. jc_selenium_helper-0.1.0/src/jc_selenium_helper.egg-info/requires.txt +27 -0
  19. jc_selenium_helper-0.1.0/src/jc_selenium_helper.egg-info/top_level.txt +1 -0
  20. jc_selenium_helper-0.1.0/tests/test_axe.py +19 -0
  21. jc_selenium_helper-0.1.0/tests/test_browser_actions.py +34 -0
  22. jc_selenium_helper-0.1.0/tests/test_browser_assertions.py +35 -0
  23. jc_selenium_helper-0.1.0/tests/test_browser_finders.py +24 -0
  24. jc_selenium_helper-0.1.0/tests/test_browser_frame.py +12 -0
  25. jc_selenium_helper-0.1.0/tests/test_browser_waits.py +29 -0
  26. jc_selenium_helper-0.1.0/tests/test_colors.py +16 -0
  27. jc_selenium_helper-0.1.0/tests/test_config.py +20 -0
  28. jc_selenium_helper-0.1.0/tests/test_legacy.py +24 -0
  29. jc_selenium_helper-0.1.0/tests/test_package.py +7 -0
  30. jc_selenium_helper-0.1.0/tests/test_plugin.py +48 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Alexander Jacob
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,82 @@
1
+ Metadata-Version: 2.4
2
+ Name: jc-selenium-helper
3
+ Version: 0.1.0
4
+ Summary: Selenium WebDriver helper utilities for writing browser tests
5
+ Author-email: Alexander Jacob <alexander.jacob@jacob-consulting.de>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/jacob-consulting/jc-selenium-helper
8
+ Project-URL: Documentation, https://jc-selenium-helper.readthedocs.io
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Programming Language :: Python :: 3.14
15
+ Classifier: Framework :: Pytest
16
+ Classifier: Topic :: Software Development :: Testing
17
+ Requires-Python: >=3.12
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: selenium>=4.38.0
21
+ Provides-Extra: pytest
22
+ Requires-Dist: pytest>=8; extra == "pytest"
23
+ Requires-Dist: pytest-selenium>=4.1.0; extra == "pytest"
24
+ Provides-Extra: config
25
+ Requires-Dist: dynaconf>=3.2; extra == "config"
26
+ Provides-Extra: axe
27
+ Requires-Dist: axe-selenium-python>=2.1.6; extra == "axe"
28
+ Provides-Extra: all
29
+ Requires-Dist: jc-selenium-helper[axe,config,pytest]; extra == "all"
30
+ Provides-Extra: dev
31
+ Requires-Dist: ruff; extra == "dev"
32
+ Requires-Dist: pre-commit; extra == "dev"
33
+ Requires-Dist: bump-my-version; extra == "dev"
34
+ Requires-Dist: mkdocs>=1.6.1; extra == "dev"
35
+ Requires-Dist: mkdocs-awesome-pages-plugin>=2.10.1; extra == "dev"
36
+ Provides-Extra: test
37
+ Requires-Dist: jc-selenium-helper[axe,config,pytest]; extra == "test"
38
+ Requires-Dist: nox; extra == "test"
39
+ Requires-Dist: pytest-cov; extra == "test"
40
+ Requires-Dist: pytest-xdist; extra == "test"
41
+ Dynamic: license-file
42
+
43
+ # jc-selenium-helper
44
+
45
+ Selenium WebDriver helper utilities for writing browser tests.
46
+
47
+ `Browser` wraps a Selenium `WebDriver` with concise, XPath-by-default finders,
48
+ waits, actions, assertions, and frame handling. Optional extras add a ready-made
49
+ pytest fixture, a Dynaconf settings loader, and axe-core accessibility checks.
50
+
51
+ ## Install
52
+
53
+ ```bash
54
+ pip install jc-selenium-helper # core (selenium only)
55
+ pip install "jc-selenium-helper[pytest]" # + pytest fixtures
56
+ pip install "jc-selenium-helper[config]" # + Dynaconf settings loader
57
+ pip install "jc-selenium-helper[axe]" # + accessibility checks
58
+ pip install "jc-selenium-helper[all]" # everything
59
+ ```
60
+
61
+ ## Quickstart
62
+
63
+ ```python
64
+ from selenium import webdriver
65
+ from jc_selenium_helper import Browser
66
+
67
+ driver = webdriver.Chrome()
68
+ browser = Browser(driver, default_timeout=30)
69
+
70
+ browser.open("https://example.com")
71
+ browser.wait_and_click("//a[@id='more']") # XPath by default
72
+ ```
73
+
74
+ ## Links
75
+
76
+ - Full documentation: https://jc-selenium-helper.readthedocs.io
77
+ - PyPI: https://pypi.org/project/jc-selenium-helper/
78
+ - Source: https://github.com/jacob-consulting/jc-selenium-helper
79
+
80
+ ## License
81
+
82
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,40 @@
1
+ # jc-selenium-helper
2
+
3
+ Selenium WebDriver helper utilities for writing browser tests.
4
+
5
+ `Browser` wraps a Selenium `WebDriver` with concise, XPath-by-default finders,
6
+ waits, actions, assertions, and frame handling. Optional extras add a ready-made
7
+ pytest fixture, a Dynaconf settings loader, and axe-core accessibility checks.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ pip install jc-selenium-helper # core (selenium only)
13
+ pip install "jc-selenium-helper[pytest]" # + pytest fixtures
14
+ pip install "jc-selenium-helper[config]" # + Dynaconf settings loader
15
+ pip install "jc-selenium-helper[axe]" # + accessibility checks
16
+ pip install "jc-selenium-helper[all]" # everything
17
+ ```
18
+
19
+ ## Quickstart
20
+
21
+ ```python
22
+ from selenium import webdriver
23
+ from jc_selenium_helper import Browser
24
+
25
+ driver = webdriver.Chrome()
26
+ browser = Browser(driver, default_timeout=30)
27
+
28
+ browser.open("https://example.com")
29
+ browser.wait_and_click("//a[@id='more']") # XPath by default
30
+ ```
31
+
32
+ ## Links
33
+
34
+ - Full documentation: https://jc-selenium-helper.readthedocs.io
35
+ - PyPI: https://pypi.org/project/jc-selenium-helper/
36
+ - Source: https://github.com/jacob-consulting/jc-selenium-helper
37
+
38
+ ## License
39
+
40
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,84 @@
1
+ [project]
2
+ name = "jc-selenium-helper"
3
+ version = "0.1.0"
4
+ description = "Selenium WebDriver helper utilities for writing browser tests"
5
+ authors = [{ name = "Alexander Jacob", email = "alexander.jacob@jacob-consulting.de" }]
6
+ license = { text = "MIT" }
7
+ readme = "README.md"
8
+ requires-python = ">=3.12"
9
+ classifiers = [
10
+ "Intended Audience :: Developers",
11
+ "Development Status :: 4 - Beta",
12
+ "License :: OSI Approved :: MIT License",
13
+ "Programming Language :: Python :: 3.12",
14
+ "Programming Language :: Python :: 3.13",
15
+ "Programming Language :: Python :: 3.14",
16
+ "Framework :: Pytest",
17
+ "Topic :: Software Development :: Testing",
18
+ ]
19
+ dependencies = [
20
+ "selenium>=4.38.0",
21
+ ]
22
+
23
+ [project.urls]
24
+ Homepage = "https://github.com/jacob-consulting/jc-selenium-helper"
25
+ Documentation = "https://jc-selenium-helper.readthedocs.io"
26
+
27
+ [project.optional-dependencies]
28
+ pytest = ["pytest>=8", "pytest-selenium>=4.1.0"]
29
+ config = ["dynaconf>=3.2"]
30
+ axe = ["axe-selenium-python>=2.1.6"]
31
+ all = ["jc-selenium-helper[pytest,config,axe]"]
32
+ dev = [
33
+ "ruff",
34
+ "pre-commit",
35
+ "bump-my-version",
36
+ "mkdocs>=1.6.1",
37
+ "mkdocs-awesome-pages-plugin>=2.10.1",
38
+ ]
39
+ test = [
40
+ "jc-selenium-helper[pytest,config,axe]",
41
+ "nox",
42
+ "pytest-cov",
43
+ "pytest-xdist",
44
+ ]
45
+
46
+ [project.entry-points.pytest11]
47
+ jc_selenium_helper = "jc_selenium_helper.plugin"
48
+
49
+ [build-system]
50
+ requires = ["setuptools>=68", "wheel"]
51
+ build-backend = "setuptools.build_meta"
52
+
53
+ [tool.setuptools.packages.find]
54
+ where = ["src"]
55
+
56
+ [tool.setuptools.package-data]
57
+ jc_selenium_helper = ["py.typed"]
58
+
59
+ [tool.ruff]
60
+ line-length = 120
61
+ target-version = "py312"
62
+
63
+ [tool.ruff.lint]
64
+ select = ["E", "F", "I", "UP", "B"]
65
+
66
+ [tool.pytest.ini_options]
67
+ testpaths = ["tests"]
68
+ addopts = "-p no:cacheprovider"
69
+
70
+ [tool.bumpversion]
71
+ current_version = "0.1.0"
72
+ allow_dirty = false
73
+ commit = true
74
+ tag = true
75
+
76
+ [[tool.bumpversion.files]]
77
+ filename = "pyproject.toml"
78
+ search = 'version = "{current_version}"'
79
+ replace = 'version = "{new_version}"'
80
+
81
+ [[tool.bumpversion.files]]
82
+ filename = "src/jc_selenium_helper/__init__.py"
83
+ search = '__version__ = "{current_version}"'
84
+ replace = '__version__ = "{new_version}"'
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,7 @@
1
+ """Selenium WebDriver helper utilities for writing browser tests."""
2
+
3
+ from jc_selenium_helper.browser import Browser
4
+
5
+ __version__ = "0.1.0"
6
+
7
+ __all__ = ["Browser", "__version__"]
@@ -0,0 +1,21 @@
1
+ """Optional accessibility checks via axe-core (requires the ``axe`` extra)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from axe_selenium_python import Axe
6
+
7
+
8
+ def run_axe(driver) -> dict:
9
+ """Inject axe-core into the current page and return the raw results dict."""
10
+ axe = Axe(driver)
11
+ axe.inject()
12
+ return axe.run()
13
+
14
+
15
+ def assert_no_violations(driver) -> None:
16
+ """Run axe-core and raise ``AssertionError`` if any violations are found."""
17
+ results = run_axe(driver)
18
+ violations = results.get("violations", [])
19
+ if violations:
20
+ ids = ", ".join(v["id"] for v in violations)
21
+ raise AssertionError(f"{len(violations)} accessibility violation(s): {ids}")
@@ -0,0 +1,190 @@
1
+ # src/jc_selenium_helper/browser.py
2
+ """A thin, ergonomic wrapper around a Selenium WebDriver."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import time
7
+
8
+ from selenium.common.exceptions import NoSuchElementException, TimeoutException
9
+ from selenium.webdriver.common.action_chains import ActionChains
10
+ from selenium.webdriver.common.by import By
11
+ from selenium.webdriver.common.keys import Keys
12
+ from selenium.webdriver.remote.webelement import WebElement
13
+ from selenium.webdriver.support import expected_conditions as EC
14
+ from selenium.webdriver.support.select import Select
15
+ from selenium.webdriver.support.wait import WebDriverWait
16
+
17
+
18
+ class Browser:
19
+ """Wraps a Selenium ``WebDriver`` with convenience helpers.
20
+
21
+ Locators default to XPath; pass ``by=By.CSS_SELECTOR`` (or any Selenium
22
+ ``By`` value) to use a different strategy.
23
+ """
24
+
25
+ def __init__(self, driver, default_timeout: float = 120, poll_pause: float = 1) -> None:
26
+ self.driver = driver
27
+ self.default_timeout = default_timeout
28
+ self.poll_pause = poll_pause
29
+
30
+ # -- navigation --
31
+ def open(self, url: str) -> None:
32
+ self.driver.get(url)
33
+
34
+ # -- finders --
35
+ def find(self, locator: str, by: str = By.XPATH) -> WebElement:
36
+ return self.driver.find_element(by, locator)
37
+
38
+ def find_all(self, locator: str, by: str = By.XPATH) -> list[WebElement]:
39
+ return self.driver.find_elements(by, locator)
40
+
41
+ def exists(self, locator: str, by: str = By.XPATH) -> bool:
42
+ try:
43
+ self.driver.find_element(by, locator)
44
+ except NoSuchElementException:
45
+ return False
46
+ return True
47
+
48
+ # -- waits --
49
+ def _timeout(self, timeout: float | None) -> float:
50
+ return self.default_timeout if timeout is None else timeout
51
+
52
+ def wait_present(self, locator: str, by: str = By.XPATH, timeout: float | None = None) -> WebElement:
53
+ WebDriverWait(self.driver, self._timeout(timeout)).until(EC.presence_of_element_located((by, locator)))
54
+ return self.driver.find_element(by, locator)
55
+
56
+ def wait_clickable(self, locator: str, by: str = By.XPATH, timeout: float | None = None) -> WebElement:
57
+ WebDriverWait(self.driver, self._timeout(timeout)).until(EC.element_to_be_clickable((by, locator)))
58
+ return self.driver.find_element(by, locator)
59
+
60
+ def wait_not_present(self, locator: str, by: str = By.XPATH, timeout: float | None = None) -> bool:
61
+ seconds = self._timeout(timeout)
62
+ try:
63
+ WebDriverWait(self.driver, seconds).until(EC.invisibility_of_element_located((by, locator)))
64
+ except TimeoutException as exc:
65
+ raise TimeoutException(f"Element '{locator}' still present after {seconds}s") from exc
66
+ return True
67
+
68
+ def wait_document_ready(self, timeout: float | None = None) -> None:
69
+ seconds = self._timeout(timeout)
70
+ elapsed = 0.0
71
+ while self.driver.execute_script("return document.readyState") != "complete":
72
+ time.sleep(self.poll_pause)
73
+ elapsed += self.poll_pause
74
+ if elapsed > seconds:
75
+ raise TimeoutError("document.readyState never reached 'complete'")
76
+
77
+ def wait_page_loaded(
78
+ self,
79
+ check_path: str,
80
+ by: str = By.XPATH,
81
+ retries: int = 5,
82
+ interval: float = 10,
83
+ ) -> None:
84
+ """Wait for ``check_path`` to appear, refreshing between attempts.
85
+
86
+ Generic replacement for the app-specific ``seite_geladen`` loop.
87
+ """
88
+ time.sleep(self.poll_pause)
89
+ attempt = 0
90
+ while not self.driver.find_elements(by, check_path):
91
+ self.driver.refresh()
92
+ time.sleep(interval)
93
+ attempt += 1
94
+ if attempt > retries:
95
+ raise TimeoutError(f"Page not loaded (missing {check_path})")
96
+ self.wait_document_ready()
97
+
98
+ # -- actions --
99
+ def wait_and_click(self, locator: str, by: str = By.XPATH, timeout: float | None = None) -> None:
100
+ self.wait_clickable(locator, by, timeout).click()
101
+
102
+ def double_click(self, locator: str, by: str = By.XPATH, pause: float = 0) -> None:
103
+ element = self.find(locator, by)
104
+ ActionChains(self.driver).move_to_element(element).double_click().perform()
105
+ if pause:
106
+ time.sleep(pause)
107
+
108
+ def hover(self, locator: str, by: str = By.XPATH) -> None:
109
+ element = self.find(locator, by)
110
+ ActionChains(self.driver).move_to_element(element).perform()
111
+
112
+ def hover_with_offset(self, locator: str, x_offset: int, y_offset: int, by: str = By.XPATH) -> None:
113
+ element = self.find(locator, by)
114
+ actions = ActionChains(self.driver)
115
+ actions.move_to_element(element).perform()
116
+ actions.move_by_offset(x_offset, y_offset).perform()
117
+
118
+ def move_to(self, locator: str, by: str = By.XPATH) -> None:
119
+ element = self.find(locator, by)
120
+ ActionChains(self.driver).move_to_element(element).perform()
121
+ time.sleep(self.poll_pause)
122
+
123
+ def wait_move_click(self, locator: str, by: str = By.XPATH, timeout: float | None = None) -> None:
124
+ element = self.wait_clickable(locator, by, timeout)
125
+ ActionChains(self.driver).move_to_element(element).perform()
126
+ time.sleep(self.poll_pause)
127
+ element.click()
128
+
129
+ def type_text(self, locator: str, text: str, by: str = By.XPATH) -> None:
130
+ self.find(locator, by).send_keys(text)
131
+
132
+ def upload_file(self, css_selector: str, path: str) -> None:
133
+ self.driver.find_element(By.CSS_SELECTOR, css_selector).send_keys(path)
134
+
135
+ def click_in_new_tab(
136
+ self,
137
+ locator: str,
138
+ check_path: str,
139
+ by: str = By.XPATH,
140
+ check_by: str = By.XPATH,
141
+ ) -> None:
142
+ """Ctrl-click a link, verify the new tab, close it, return to the first tab."""
143
+ element = self.find(locator, by)
144
+ ActionChains(self.driver).key_down(Keys.CONTROL).click(element).key_up(Keys.CONTROL).perform()
145
+ self.driver.switch_to.window(self.driver.window_handles[-1])
146
+ self.wait_present(check_path, check_by)
147
+ self.driver.close()
148
+ self.driver.switch_to.window(self.driver.window_handles[0])
149
+
150
+ # -- assertions --
151
+ def assert_checkbox_checked(self, locator: str, by: str = By.XPATH, timeout: float | None = None) -> None:
152
+ if not self.wait_present(locator, by, timeout).is_selected():
153
+ raise AssertionError(f"Checkbox is not checked: {locator}")
154
+
155
+ def assert_checkbox_unchecked(self, locator: str, by: str = By.XPATH, timeout: float | None = None) -> None:
156
+ if self.wait_present(locator, by, timeout).is_selected():
157
+ raise AssertionError(f"Checkbox is checked: {locator}")
158
+
159
+ def assert_selected_option(self, locator: str, expected_text: str, by: str = By.XPATH) -> None:
160
+ select = Select(self.find(locator, by))
161
+ actual = select.first_selected_option.text
162
+ if actual != expected_text:
163
+ raise AssertionError(f"Selected option '{actual}' != expected '{expected_text}'")
164
+
165
+ def assert_present(self, locator: str, by: str = By.XPATH) -> None:
166
+ if not self.exists(locator, by):
167
+ raise AssertionError(f"Element not present: {locator}")
168
+
169
+ # -- frames --
170
+ def fill_in_frame(
171
+ self,
172
+ frame_path: str,
173
+ inner_path: str,
174
+ text: str,
175
+ by: str = By.XPATH,
176
+ submit: bool = True,
177
+ ) -> None:
178
+ """Switch into ``frame_path``, type ``text`` into ``inner_path``, switch back.
179
+
180
+ Generic replacement for the app-specific ``switch_and_fill_frame`` (which
181
+ hardcoded the TinyMCE inner path).
182
+ """
183
+ frame = self.find(frame_path, by)
184
+ self.driver.switch_to.frame(frame)
185
+ try:
186
+ self.type_text(inner_path, text)
187
+ if submit:
188
+ self.type_text(inner_path, Keys.RETURN)
189
+ finally:
190
+ self.driver.switch_to.default_content()
@@ -0,0 +1,14 @@
1
+ """Color conversion helpers (replaces the colormap/easydev dependency)."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ def rgb_to_hex(rgb_string: str) -> str:
7
+ """Convert a CSS ``rgb(...)`` or ``rgba(...)`` string to ``#rrggbb``.
8
+
9
+ The alpha channel of ``rgba`` is ignored.
10
+ """
11
+ inner = rgb_string[rgb_string.index("(") + 1 : rgb_string.index(")")]
12
+ parts = [int(float(component.strip())) for component in inner.split(",")[:3]]
13
+ r, g, b = parts
14
+ return f"#{r:02x}{g:02x}{b:02x}"
@@ -0,0 +1,28 @@
1
+ """Optional Dynaconf settings loader (requires the ``config`` extra)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from dynaconf import Dynaconf
8
+ from dynaconf.base import Settings
9
+
10
+
11
+ def get_settings(location: str | Path, *files: str) -> Settings:
12
+ """Build a Dynaconf ``Settings`` from files resolved next to ``location``.
13
+
14
+ ``location`` is typically ``__file__`` of the caller; each name in ``files``
15
+ is resolved against ``Path(location).parent`` and must exist.
16
+ """
17
+ parent = Path(location).parent
18
+ settings_files = []
19
+ for name in files:
20
+ settings_file = (parent / name).resolve()
21
+ assert settings_file.exists(), f"settings file not found: {settings_file}"
22
+ settings_files.append(settings_file)
23
+ return Dynaconf(
24
+ environments=True,
25
+ envvar_prefix="SELENIUM",
26
+ env_switcher="SELENIUM_ENVIRONMENT",
27
+ settings_files=settings_files,
28
+ )
@@ -0,0 +1,158 @@
1
+ # src/jc_selenium_helper/legacy.py
2
+ """Backwards-compatible adapter exposing the original German/ad-hoc API.
3
+
4
+ Existing suites can migrate by replacing::
5
+
6
+ from libs.browser import Browser
7
+
8
+ with::
9
+
10
+ from jc_selenium_helper.legacy import Browser
11
+
12
+ Every legacy method emits a ``DeprecationWarning`` and delegates to the clean
13
+ :class:`jc_selenium_helper.browser.Browser` API. Two methods
14
+ (``seite_geladen`` and ``switch_and_fill_frame``) keep their original
15
+ app-specific behavior verbatim so existing tests are unaffected.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import time
21
+ import warnings
22
+
23
+ from selenium.webdriver.common.by import By
24
+ from selenium.webdriver.common.keys import Keys
25
+
26
+ from jc_selenium_helper.browser import Browser as _Browser
27
+ from jc_selenium_helper.colors import rgb_to_hex
28
+
29
+
30
+ def _deprecated(old: str, new: str) -> None:
31
+ warnings.warn(
32
+ f"{old}() is deprecated; use {new}() instead.",
33
+ DeprecationWarning,
34
+ stacklevel=3,
35
+ )
36
+
37
+
38
+ class LegacyBrowser(_Browser):
39
+ """Deprecated German-named API preserved for existing test suites."""
40
+
41
+ def xpath(self, xpath):
42
+ _deprecated("xpath", "find")
43
+ return self.find(xpath)
44
+
45
+ def xpaths(self, xpath):
46
+ _deprecated("xpaths", "find_all")
47
+ return self.find_all(xpath)
48
+
49
+ def css(self, selector):
50
+ _deprecated("css", "find")
51
+ return self.find(selector, by=By.CSS_SELECTOR)
52
+
53
+ def get_elements(self, path):
54
+ _deprecated("get_elements", "find_all")
55
+ return self.find_all(path)
56
+
57
+ def check_exists_by_xpath(self, path):
58
+ _deprecated("check_exists_by_xpath", "exists")
59
+ return self.exists(path)
60
+
61
+ def wait_element_present(self, path, time_to_wait=None):
62
+ _deprecated("wait_element_present", "wait_present")
63
+ return self.wait_present(path, timeout=time_to_wait)
64
+
65
+ def wait_element_not_present(self, path, time_to_wait=None):
66
+ _deprecated("wait_element_not_present", "wait_not_present")
67
+ return self.wait_not_present(path, timeout=time_to_wait)
68
+
69
+ def wait_element_clickable(self, path, time_to_wait=None):
70
+ _deprecated("wait_element_clickable", "wait_clickable")
71
+ return self.wait_clickable(path, timeout=time_to_wait)
72
+
73
+ def inhalt_geladen(self):
74
+ _deprecated("inhalt_geladen", "wait_document_ready")
75
+ return self.wait_document_ready()
76
+
77
+ def doppelclick_element(self, pause, hover):
78
+ _deprecated("doppelclick_element", "double_click")
79
+ return self.double_click(hover, pause=pause)
80
+
81
+ def hover_element(self, hover):
82
+ _deprecated("hover_element", "hover")
83
+ return self.hover(hover)
84
+
85
+ def hover_element_with_offset(self, hover, x_offset, y_offset):
86
+ _deprecated("hover_element_with_offset", "hover_with_offset")
87
+ return self.hover_with_offset(hover, x_offset, y_offset)
88
+
89
+ def move_to_element(self, path):
90
+ _deprecated("move_to_element", "move_to")
91
+ return self.move_to(path)
92
+
93
+ def wait_move_click_element(self, path, time_to_wait=None):
94
+ _deprecated("wait_move_click_element", "wait_move_click")
95
+ return self.wait_move_click(path, timeout=time_to_wait)
96
+
97
+ def click_new_tab(self, path, check_path):
98
+ _deprecated("click_new_tab", "click_in_new_tab")
99
+ return self.click_in_new_tab(path, check_path)
100
+
101
+ def eingabe(self, path, text):
102
+ _deprecated("eingabe", "type_text")
103
+ return self.type_text(path, text)
104
+
105
+ def eingabe_upload_css(self, path, text):
106
+ _deprecated("eingabe_upload_css", "upload_file")
107
+ return self.upload_file(path, text)
108
+
109
+ def assert_checkbox_is_checked(self, path, time_to_wait=None):
110
+ _deprecated("assert_checkbox_is_checked", "assert_checkbox_checked")
111
+ return self.assert_checkbox_checked(path, timeout=time_to_wait)
112
+
113
+ def assert_checkbox_is_not_checked(self, path, time_to_wait=None):
114
+ _deprecated("assert_checkbox_is_not_checked", "assert_checkbox_unchecked")
115
+ return self.assert_checkbox_unchecked(path, timeout=time_to_wait)
116
+
117
+ def check_select(self, path, exp_text):
118
+ _deprecated("check_select", "assert_selected_option")
119
+ return self.assert_selected_option(path, exp_text)
120
+
121
+ def ele_test(self, path):
122
+ _deprecated("ele_test", "assert_present")
123
+ return self.assert_present(path)
124
+
125
+ @staticmethod
126
+ def switch_rgb(color_rgb):
127
+ _deprecated("switch_rgb", "colors.rgb_to_hex")
128
+ return rgb_to_hex(color_rgb)
129
+
130
+ # -- verbatim app-specific behavior (unchanged for compatibility) --
131
+ def seite_geladen(self, check_path):
132
+ _deprecated("seite_geladen", "wait_page_loaded")
133
+ time_counter = 0
134
+ time.sleep(1)
135
+ while True:
136
+ element = self.driver.find_elements(By.XPATH, check_path)
137
+ if element:
138
+ break
139
+ self.driver.refresh()
140
+ time.sleep(10)
141
+ time_counter += 1
142
+ if time_counter > 5:
143
+ raise ValueError("Seite nicht geladen")
144
+ self.inhalt_geladen()
145
+
146
+ def switch_and_fill_frame(self, combined, path, text):
147
+ _deprecated("switch_and_fill_frame", "fill_in_frame")
148
+ time.sleep(combined.settings.k_pause)
149
+ switch_frame = self.driver.find_element(By.XPATH, path)
150
+ self.driver.switch_to.frame(switch_frame)
151
+ tmp_path = "//*[@id='tinymce']/p"
152
+ self.type_text(tmp_path, text)
153
+ self.type_text(tmp_path, Keys.RETURN)
154
+ self.driver.switch_to.default_content()
155
+
156
+
157
+ # Drop-in module-level alias: ``from jc_selenium_helper.legacy import Browser``
158
+ Browser = LegacyBrowser
@@ -0,0 +1,43 @@
1
+ """pytest plugin exposing ready-made fixtures.
2
+
3
+ Enabled automatically when the package is installed with the ``pytest`` extra
4
+ (registered via the ``pytest11`` entry point). Fixtures are namespaced with a
5
+ ``jc_`` prefix to avoid clashing with a project's own ``browser`` fixture.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import pytest
11
+
12
+ from jc_selenium_helper.browser import Browser
13
+
14
+
15
+ @pytest.fixture
16
+ def jc_chrome_options():
17
+ """Sensible default Chrome options; override in a project conftest to customize."""
18
+ from selenium.webdriver.chrome.options import Options
19
+
20
+ options = Options()
21
+ options.add_argument("--headless=new")
22
+ options.add_argument("--disable-gpu")
23
+ options.add_argument("--ignore-certificate-errors")
24
+ options.add_argument("--no-sandbox")
25
+ options.add_argument("--disable-dev-shm-usage")
26
+ return options
27
+
28
+
29
+ @pytest.fixture
30
+ def chrome_options(jc_chrome_options):
31
+ """Feed jc_chrome_options into pytest-selenium's driver.
32
+
33
+ pytest-selenium builds its Chrome driver from a fixture named
34
+ ``chrome_options``; delegating here makes the package's defaults apply while
35
+ letting users customize by overriding ``jc_chrome_options``.
36
+ """
37
+ return jc_chrome_options
38
+
39
+
40
+ @pytest.fixture
41
+ def jc_browser(selenium) -> Browser:
42
+ """Wrap the pytest-selenium ``selenium`` driver in a :class:`Browser`."""
43
+ return Browser(selenium)
@@ -0,0 +1,82 @@
1
+ Metadata-Version: 2.4
2
+ Name: jc-selenium-helper
3
+ Version: 0.1.0
4
+ Summary: Selenium WebDriver helper utilities for writing browser tests
5
+ Author-email: Alexander Jacob <alexander.jacob@jacob-consulting.de>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/jacob-consulting/jc-selenium-helper
8
+ Project-URL: Documentation, https://jc-selenium-helper.readthedocs.io
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Programming Language :: Python :: 3.14
15
+ Classifier: Framework :: Pytest
16
+ Classifier: Topic :: Software Development :: Testing
17
+ Requires-Python: >=3.12
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: selenium>=4.38.0
21
+ Provides-Extra: pytest
22
+ Requires-Dist: pytest>=8; extra == "pytest"
23
+ Requires-Dist: pytest-selenium>=4.1.0; extra == "pytest"
24
+ Provides-Extra: config
25
+ Requires-Dist: dynaconf>=3.2; extra == "config"
26
+ Provides-Extra: axe
27
+ Requires-Dist: axe-selenium-python>=2.1.6; extra == "axe"
28
+ Provides-Extra: all
29
+ Requires-Dist: jc-selenium-helper[axe,config,pytest]; extra == "all"
30
+ Provides-Extra: dev
31
+ Requires-Dist: ruff; extra == "dev"
32
+ Requires-Dist: pre-commit; extra == "dev"
33
+ Requires-Dist: bump-my-version; extra == "dev"
34
+ Requires-Dist: mkdocs>=1.6.1; extra == "dev"
35
+ Requires-Dist: mkdocs-awesome-pages-plugin>=2.10.1; extra == "dev"
36
+ Provides-Extra: test
37
+ Requires-Dist: jc-selenium-helper[axe,config,pytest]; extra == "test"
38
+ Requires-Dist: nox; extra == "test"
39
+ Requires-Dist: pytest-cov; extra == "test"
40
+ Requires-Dist: pytest-xdist; extra == "test"
41
+ Dynamic: license-file
42
+
43
+ # jc-selenium-helper
44
+
45
+ Selenium WebDriver helper utilities for writing browser tests.
46
+
47
+ `Browser` wraps a Selenium `WebDriver` with concise, XPath-by-default finders,
48
+ waits, actions, assertions, and frame handling. Optional extras add a ready-made
49
+ pytest fixture, a Dynaconf settings loader, and axe-core accessibility checks.
50
+
51
+ ## Install
52
+
53
+ ```bash
54
+ pip install jc-selenium-helper # core (selenium only)
55
+ pip install "jc-selenium-helper[pytest]" # + pytest fixtures
56
+ pip install "jc-selenium-helper[config]" # + Dynaconf settings loader
57
+ pip install "jc-selenium-helper[axe]" # + accessibility checks
58
+ pip install "jc-selenium-helper[all]" # everything
59
+ ```
60
+
61
+ ## Quickstart
62
+
63
+ ```python
64
+ from selenium import webdriver
65
+ from jc_selenium_helper import Browser
66
+
67
+ driver = webdriver.Chrome()
68
+ browser = Browser(driver, default_timeout=30)
69
+
70
+ browser.open("https://example.com")
71
+ browser.wait_and_click("//a[@id='more']") # XPath by default
72
+ ```
73
+
74
+ ## Links
75
+
76
+ - Full documentation: https://jc-selenium-helper.readthedocs.io
77
+ - PyPI: https://pypi.org/project/jc-selenium-helper/
78
+ - Source: https://github.com/jacob-consulting/jc-selenium-helper
79
+
80
+ ## License
81
+
82
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,28 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/jc_selenium_helper/__init__.py
5
+ src/jc_selenium_helper/axe.py
6
+ src/jc_selenium_helper/browser.py
7
+ src/jc_selenium_helper/colors.py
8
+ src/jc_selenium_helper/config.py
9
+ src/jc_selenium_helper/legacy.py
10
+ src/jc_selenium_helper/plugin.py
11
+ src/jc_selenium_helper/py.typed
12
+ src/jc_selenium_helper.egg-info/PKG-INFO
13
+ src/jc_selenium_helper.egg-info/SOURCES.txt
14
+ src/jc_selenium_helper.egg-info/dependency_links.txt
15
+ src/jc_selenium_helper.egg-info/entry_points.txt
16
+ src/jc_selenium_helper.egg-info/requires.txt
17
+ src/jc_selenium_helper.egg-info/top_level.txt
18
+ tests/test_axe.py
19
+ tests/test_browser_actions.py
20
+ tests/test_browser_assertions.py
21
+ tests/test_browser_finders.py
22
+ tests/test_browser_frame.py
23
+ tests/test_browser_waits.py
24
+ tests/test_colors.py
25
+ tests/test_config.py
26
+ tests/test_legacy.py
27
+ tests/test_package.py
28
+ tests/test_plugin.py
@@ -0,0 +1,2 @@
1
+ [pytest11]
2
+ jc_selenium_helper = jc_selenium_helper.plugin
@@ -0,0 +1,27 @@
1
+ selenium>=4.38.0
2
+
3
+ [all]
4
+ jc-selenium-helper[axe,config,pytest]
5
+
6
+ [axe]
7
+ axe-selenium-python>=2.1.6
8
+
9
+ [config]
10
+ dynaconf>=3.2
11
+
12
+ [dev]
13
+ ruff
14
+ pre-commit
15
+ bump-my-version
16
+ mkdocs>=1.6.1
17
+ mkdocs-awesome-pages-plugin>=2.10.1
18
+
19
+ [pytest]
20
+ pytest>=8
21
+ pytest-selenium>=4.1.0
22
+
23
+ [test]
24
+ jc-selenium-helper[axe,config,pytest]
25
+ nox
26
+ pytest-cov
27
+ pytest-xdist
@@ -0,0 +1 @@
1
+ jc_selenium_helper
@@ -0,0 +1,19 @@
1
+ # tests/test_axe.py
2
+ import pytest
3
+
4
+ pytest.importorskip("axe_selenium_python")
5
+
6
+ from jc_selenium_helper.axe import assert_no_violations, run_axe # noqa: E402
7
+
8
+
9
+ def test_run_axe_returns_results(driver, fixture_url):
10
+ driver.get(fixture_url("basic.html"))
11
+ results = run_axe(driver)
12
+ assert "violations" in results
13
+
14
+
15
+ def test_assert_no_violations_raises_on_violation(driver, fixture_url):
16
+ # a11y_bad.html has an <img> with no alt attribute -> at least one violation expected
17
+ driver.get(fixture_url("a11y_bad.html"))
18
+ with pytest.raises(AssertionError):
19
+ assert_no_violations(driver)
@@ -0,0 +1,34 @@
1
+ # tests/test_browser_actions.py
2
+ def test_type_text(browser, fixture_url):
3
+ browser.open(fixture_url("basic.html"))
4
+ browser.type_text("//input[@id='text-input']", "hello")
5
+ assert browser.find("//input[@id='text-input']").get_attribute("value") == "hello"
6
+
7
+
8
+ def test_wait_and_click(browser, fixture_url):
9
+ browser.open(fixture_url("basic.html"))
10
+ browser.wait_and_click("//button[@id='clickable']")
11
+ assert browser.find("//button[@id='clickable']").text == "clicked"
12
+
13
+
14
+ def test_double_click(browser, fixture_url):
15
+ browser.open(fixture_url("basic.html"))
16
+ browser.double_click("//button[@id='dbl']")
17
+ assert browser.find("//button[@id='dbl']").text == "double"
18
+
19
+
20
+ def test_hover_does_not_raise(browser, fixture_url):
21
+ browser.open(fixture_url("basic.html"))
22
+ browser.hover("//button[@id='clickable']")
23
+
24
+
25
+ def test_move_to_does_not_raise(browser, fixture_url):
26
+ browser.open(fixture_url("basic.html"))
27
+ browser.move_to("//button[@id='clickable']")
28
+
29
+
30
+ def test_click_in_new_tab_returns_to_main(browser, fixture_url):
31
+ browser.open(fixture_url("basic.html"))
32
+ browser.click_in_new_tab("//a[@id='newtab']", "//p[@id='target']")
33
+ assert len(browser.driver.window_handles) == 1
34
+ assert browser.find("//h1[@id='title']").text == "Hello"
@@ -0,0 +1,35 @@
1
+ # tests/test_browser_assertions.py
2
+ import pytest
3
+
4
+
5
+ def test_assert_checkbox_checked_passes(browser, fixture_url):
6
+ browser.open(fixture_url("basic.html"))
7
+ browser.assert_checkbox_checked("//input[@id='checked-box']")
8
+
9
+
10
+ def test_assert_checkbox_checked_fails(browser, fixture_url):
11
+ browser.open(fixture_url("basic.html"))
12
+ with pytest.raises(AssertionError):
13
+ browser.assert_checkbox_checked("//input[@id='unchecked-box']")
14
+
15
+
16
+ def test_assert_checkbox_unchecked_passes(browser, fixture_url):
17
+ browser.open(fixture_url("basic.html"))
18
+ browser.assert_checkbox_unchecked("//input[@id='unchecked-box']")
19
+
20
+
21
+ def test_assert_selected_option_passes(browser, fixture_url):
22
+ browser.open(fixture_url("basic.html"))
23
+ browser.assert_selected_option("//select[@id='picker']", "two")
24
+
25
+
26
+ def test_assert_selected_option_fails(browser, fixture_url):
27
+ browser.open(fixture_url("basic.html"))
28
+ with pytest.raises(AssertionError):
29
+ browser.assert_selected_option("//select[@id='picker']", "one")
30
+
31
+
32
+ def test_assert_present_fails_when_missing(browser, fixture_url):
33
+ browser.open(fixture_url("basic.html"))
34
+ with pytest.raises(AssertionError):
35
+ browser.assert_present("//div[@id='nope']")
@@ -0,0 +1,24 @@
1
+ # tests/test_browser_finders.py
2
+ from selenium.webdriver.common.by import By
3
+
4
+
5
+ def test_find_returns_element(browser, fixture_url):
6
+ browser.open(fixture_url("basic.html"))
7
+ assert browser.find("//h1[@id='title']").text == "Hello"
8
+
9
+
10
+ def test_find_with_css(browser, fixture_url):
11
+ browser.open(fixture_url("basic.html"))
12
+ assert browser.find("#title", by=By.CSS_SELECTOR).text == "Hello"
13
+
14
+
15
+ def test_find_all_returns_list(browser, fixture_url):
16
+ browser.open(fixture_url("basic.html"))
17
+ items = browser.find_all("//li[@class='item']")
18
+ assert [e.text for e in items] == ["a", "b", "c"]
19
+
20
+
21
+ def test_exists_true_and_false(browser, fixture_url):
22
+ browser.open(fixture_url("basic.html"))
23
+ assert browser.exists("//h1[@id='title']") is True
24
+ assert browser.exists("//h1[@id='missing']") is False
@@ -0,0 +1,12 @@
1
+ # tests/test_browser_frame.py
2
+ def test_fill_in_frame(browser, fixture_url):
3
+ browser.open(fixture_url("frame.html"))
4
+ browser.fill_in_frame(
5
+ "//iframe[@id='editor']",
6
+ "//input[@id='inner-input']",
7
+ "typed",
8
+ submit=False,
9
+ )
10
+ browser.driver.switch_to.frame(browser.find("//iframe[@id='editor']"))
11
+ assert browser.find("//input[@id='inner-input']").get_attribute("value") == "typed"
12
+ browser.driver.switch_to.default_content()
@@ -0,0 +1,29 @@
1
+ # tests/test_browser_waits.py
2
+ import pytest
3
+ from selenium.common.exceptions import TimeoutException
4
+
5
+
6
+ def test_wait_present_returns_element(browser, fixture_url):
7
+ browser.open(fixture_url("basic.html"))
8
+ assert browser.wait_present("//span[@id='late']").text == "here"
9
+
10
+
11
+ def test_wait_present_times_out(browser, fixture_url):
12
+ browser.open(fixture_url("basic.html"))
13
+ with pytest.raises(TimeoutException):
14
+ browser.wait_present("//span[@id='never']", timeout=1)
15
+
16
+
17
+ def test_wait_clickable_returns_element(browser, fixture_url):
18
+ browser.open(fixture_url("basic.html"))
19
+ assert browser.wait_clickable("//button[@id='btn']").tag_name == "button"
20
+
21
+
22
+ def test_wait_document_ready(browser, fixture_url):
23
+ browser.open(fixture_url("basic.html"))
24
+ browser.wait_document_ready() # should not raise
25
+
26
+
27
+ def test_wait_page_loaded_on_present_element(browser, fixture_url):
28
+ browser.open(fixture_url("basic.html"))
29
+ browser.wait_page_loaded("//h1[@id='title']", retries=1, interval=1)
@@ -0,0 +1,16 @@
1
+ import pytest
2
+
3
+ from jc_selenium_helper.colors import rgb_to_hex
4
+
5
+
6
+ @pytest.mark.parametrize(
7
+ "value, expected",
8
+ [
9
+ ("rgb(255, 0, 0)", "#ff0000"),
10
+ ("rgb(0, 128, 0)", "#008000"),
11
+ ("rgba(16, 32, 48, 0.5)", "#102030"),
12
+ ("rgb(0,0,0)", "#000000"),
13
+ ],
14
+ )
15
+ def test_rgb_to_hex(value, expected):
16
+ assert rgb_to_hex(value) == expected
@@ -0,0 +1,20 @@
1
+ import pytest
2
+
3
+ pytest.importorskip("dynaconf")
4
+
5
+ from jc_selenium_helper.config import get_settings # noqa: E402
6
+
7
+
8
+ def test_get_settings_reads_relative_file(tmp_path):
9
+ (tmp_path / "settings.yaml").write_text("default:\n greeting: hi\n")
10
+ anchor = tmp_path / "conftest.py" # any file whose parent holds the settings
11
+ anchor.write_text("")
12
+ settings = get_settings(anchor, "settings.yaml")
13
+ assert settings.greeting == "hi"
14
+
15
+
16
+ def test_get_settings_missing_file_raises(tmp_path):
17
+ anchor = tmp_path / "conftest.py"
18
+ anchor.write_text("")
19
+ with pytest.raises(AssertionError):
20
+ get_settings(anchor, "nope.yaml")
@@ -0,0 +1,24 @@
1
+ # tests/test_legacy.py
2
+ import pytest
3
+
4
+ from jc_selenium_helper.legacy import Browser as LegacyBrowser
5
+
6
+
7
+ def test_legacy_eingabe_delegates_and_warns(driver, fixture_url):
8
+ legacy = LegacyBrowser(driver, default_timeout=10, poll_pause=0.2)
9
+ legacy.open(fixture_url("basic.html"))
10
+ with pytest.warns(DeprecationWarning):
11
+ legacy.eingabe("//input[@id='text-input']", "legacy")
12
+ assert legacy.find("//input[@id='text-input']").get_attribute("value") == "legacy"
13
+
14
+
15
+ def test_legacy_check_exists_by_xpath(driver, fixture_url):
16
+ legacy = LegacyBrowser(driver, default_timeout=10, poll_pause=0.2)
17
+ legacy.open(fixture_url("basic.html"))
18
+ with pytest.warns(DeprecationWarning):
19
+ assert legacy.check_exists_by_xpath("//h1[@id='title']") is True
20
+
21
+
22
+ def test_legacy_switch_rgb_static():
23
+ with pytest.warns(DeprecationWarning):
24
+ assert LegacyBrowser.switch_rgb("rgb(255, 0, 0)") == "#ff0000"
@@ -0,0 +1,7 @@
1
+ # tests/test_package.py
2
+ import jc_selenium_helper
3
+
4
+
5
+ def test_version_is_exposed():
6
+ assert isinstance(jc_selenium_helper.__version__, str)
7
+ assert jc_selenium_helper.__version__ == "0.1.0"
@@ -0,0 +1,48 @@
1
+ pytest_plugins = ["pytester"]
2
+
3
+
4
+ def test_jc_chrome_options_fixture_available(pytester):
5
+ pytester.makepyfile(
6
+ """
7
+ def test_options(jc_chrome_options):
8
+ args = jc_chrome_options.arguments
9
+ assert "--headless=new" in args
10
+ """
11
+ )
12
+ result = pytester.runpytest()
13
+ result.assert_outcomes(passed=1)
14
+
15
+
16
+ def test_chrome_options_feeds_pytest_selenium(pytester):
17
+ pytester.makepyfile(
18
+ """
19
+ def test_same_object(chrome_options, jc_chrome_options):
20
+ assert chrome_options is jc_chrome_options
21
+ """
22
+ )
23
+ result = pytester.runpytest()
24
+ result.assert_outcomes(passed=1)
25
+
26
+
27
+ def test_chrome_options_reflects_jc_chrome_options_override(pytester):
28
+ pytester.makeconftest(
29
+ """
30
+ import pytest
31
+ from selenium.webdriver.chrome.options import Options
32
+
33
+
34
+ @pytest.fixture
35
+ def jc_chrome_options():
36
+ options = Options()
37
+ options.add_argument("--window-size=1920,1080")
38
+ return options
39
+ """
40
+ )
41
+ pytester.makepyfile(
42
+ """
43
+ def test_override_flows_through(chrome_options):
44
+ assert "--window-size=1920,1080" in chrome_options.arguments
45
+ """
46
+ )
47
+ result = pytester.runpytest()
48
+ result.assert_outcomes(passed=1)