seleniumboot-mcp 0.2.0__tar.gz → 0.2.2__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 (23) hide show
  1. {seleniumboot_mcp-0.2.0 → seleniumboot_mcp-0.2.2}/PKG-INFO +4 -3
  2. {seleniumboot_mcp-0.2.0 → seleniumboot_mcp-0.2.2}/README.md +2 -1
  3. {seleniumboot_mcp-0.2.0 → seleniumboot_mcp-0.2.2}/pyproject.toml +3 -3
  4. {seleniumboot_mcp-0.2.0 → seleniumboot_mcp-0.2.2}/src/selenium_mcp/server.py +6 -1
  5. seleniumboot_mcp-0.2.2/src/selenium_mcp/tools/_locators.py +14 -0
  6. {seleniumboot_mcp-0.2.0/src → seleniumboot_mcp-0.2.2/src/selenium_mcp}/tools/assertion_tools.py +2 -12
  7. {seleniumboot_mcp-0.2.0/src → seleniumboot_mcp-0.2.2/src/selenium_mcp}/tools/browser_tools.py +4 -3
  8. {seleniumboot_mcp-0.2.0 → seleniumboot_mcp-0.2.2}/src/selenium_mcp/tools/codegen_tools.py +19 -4
  9. {seleniumboot_mcp-0.2.0/src → seleniumboot_mcp-0.2.2/src/selenium_mcp}/tools/element_tools.py +7 -15
  10. {seleniumboot_mcp-0.2.0 → seleniumboot_mcp-0.2.2}/src/seleniumboot_mcp.egg-info/PKG-INFO +4 -3
  11. {seleniumboot_mcp-0.2.0 → seleniumboot_mcp-0.2.2}/src/seleniumboot_mcp.egg-info/SOURCES.txt +2 -5
  12. seleniumboot_mcp-0.2.2/src/seleniumboot_mcp.egg-info/entry_points.txt +2 -0
  13. {seleniumboot_mcp-0.2.0 → seleniumboot_mcp-0.2.2}/src/seleniumboot_mcp.egg-info/top_level.txt +0 -1
  14. seleniumboot_mcp-0.2.0/src/selenium_mcp/tools/assertion_tools.py +0 -245
  15. seleniumboot_mcp-0.2.0/src/selenium_mcp/tools/browser_tools.py +0 -245
  16. seleniumboot_mcp-0.2.0/src/selenium_mcp/tools/element_tools.py +0 -364
  17. seleniumboot_mcp-0.2.0/src/seleniumboot_mcp.egg-info/entry_points.txt +0 -2
  18. seleniumboot_mcp-0.2.0/src/tools/codegen_tools.py +0 -970
  19. {seleniumboot_mcp-0.2.0 → seleniumboot_mcp-0.2.2}/setup.cfg +0 -0
  20. {seleniumboot_mcp-0.2.0 → seleniumboot_mcp-0.2.2}/src/selenium_mcp/__init__.py +0 -0
  21. {seleniumboot_mcp-0.2.0 → seleniumboot_mcp-0.2.2}/src/selenium_mcp/tools/__init__.py +0 -0
  22. {seleniumboot_mcp-0.2.0 → seleniumboot_mcp-0.2.2}/src/seleniumboot_mcp.egg-info/dependency_links.txt +0 -0
  23. {seleniumboot_mcp-0.2.0 → seleniumboot_mcp-0.2.2}/src/seleniumboot_mcp.egg-info/requires.txt +0 -0
@@ -1,8 +1,8 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: seleniumboot-mcp
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: A Python MCP server for Selenium WebDriver — browser automation, Java TestNG/JUnit5/Cucumber codegen, and Page Object Model generation
5
- Author-email: Panjatan <panjatan.tech@gmail.com>
5
+ Author-email: Raza Tech <razatechnologyservices@gmail.com>
6
6
  License: MIT
7
7
  Project-URL: Homepage, https://github.com/seleniumboot/selenium-mcp
8
8
  Project-URL: Repository, https://github.com/seleniumboot/selenium-mcp
@@ -25,6 +25,7 @@ Requires-Dist: selenium>=4.6.0
25
25
  A Python **Model Context Protocol (MCP)** server for Selenium WebDriver automation.
26
26
  Let Claude or GitHub Copilot control a real browser — navigate pages, interact with elements,
27
27
  run assertions, and generate ready-to-run **Java TestNG / JUnit 5 / Cucumber / pytest** test code from recorded sessions.
28
+ 43 tools. No ChromeDriver setup. Browser auto-starts on first use.
28
29
 
29
30
  [![PyPI](https://img.shields.io/pypi/v/seleniumboot-mcp)](https://pypi.org/project/seleniumboot-mcp/)
30
31
  [![Python](https://img.shields.io/pypi/pyversions/seleniumboot-mcp)](https://pypi.org/project/seleniumboot-mcp/)
@@ -106,7 +107,7 @@ Claude controls the real browser, records every action, and on request generates
106
107
 
107
108
  ---
108
109
 
109
- ## Tools (39 total)
110
+ ## Tools (43 total)
110
111
 
111
112
  ### Browser
112
113
  | Tool | Description |
@@ -3,6 +3,7 @@
3
3
  A Python **Model Context Protocol (MCP)** server for Selenium WebDriver automation.
4
4
  Let Claude or GitHub Copilot control a real browser — navigate pages, interact with elements,
5
5
  run assertions, and generate ready-to-run **Java TestNG / JUnit 5 / Cucumber / pytest** test code from recorded sessions.
6
+ 43 tools. No ChromeDriver setup. Browser auto-starts on first use.
6
7
 
7
8
  [![PyPI](https://img.shields.io/pypi/v/seleniumboot-mcp)](https://pypi.org/project/seleniumboot-mcp/)
8
9
  [![Python](https://img.shields.io/pypi/pyversions/seleniumboot-mcp)](https://pypi.org/project/seleniumboot-mcp/)
@@ -84,7 +85,7 @@ Claude controls the real browser, records every action, and on request generates
84
85
 
85
86
  ---
86
87
 
87
- ## Tools (39 total)
88
+ ## Tools (43 total)
88
89
 
89
90
  ### Browser
90
91
  | Tool | Description |
@@ -4,12 +4,12 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "seleniumboot-mcp"
7
- version = "0.2.0"
7
+ version = "0.2.2"
8
8
  description = "A Python MCP server for Selenium WebDriver — browser automation, Java TestNG/JUnit5/Cucumber codegen, and Page Object Model generation"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
11
11
  license = { text = "MIT" }
12
- authors = [{ name = "Panjatan", email = "panjatan.tech@gmail.com" }]
12
+ authors = [{ name = "Raza Tech", email = "razatechnologyservices@gmail.com" }]
13
13
  keywords = ["selenium", "mcp", "test-automation", "webdriver", "testng", "junit", "model-context-protocol"]
14
14
  classifiers = [
15
15
  "Development Status :: 4 - Beta",
@@ -31,7 +31,7 @@ Repository = "https://github.com/seleniumboot/selenium-mcp"
31
31
  Issues = "https://github.com/seleniumboot/selenium-mcp/issues"
32
32
 
33
33
  [project.scripts]
34
- seleniumboot-mcp = "selenium_mcp.server:main"
34
+ seleniumboot-mcp = "selenium_mcp.server:run"
35
35
 
36
36
  [tool.setuptools.packages.find]
37
37
  where = ["src"]
@@ -18,6 +18,7 @@ from selenium_mcp.tools.assertion_tools import AssertionTools
18
18
  from selenium_mcp.tools.codegen_tools import CodegenTools
19
19
 
20
20
  _log_path = Path(tempfile.gettempdir()) / "selenium-mcp.log"
21
+ _log_path.touch(mode=0o600, exist_ok=True)
21
22
  logging.basicConfig(
22
23
  filename=str(_log_path),
23
24
  level=logging.DEBUG,
@@ -74,5 +75,9 @@ async def main():
74
75
  await app.run(r, w, app.create_initialization_options())
75
76
 
76
77
 
77
- if __name__ == "__main__":
78
+ def run():
78
79
  asyncio.run(main())
80
+
81
+
82
+ if __name__ == "__main__":
83
+ run()
@@ -0,0 +1,14 @@
1
+ """Shared locator strategy map — imported by element_tools and assertion_tools."""
2
+
3
+ from selenium.webdriver.common.by import By
4
+
5
+ BY_MAP = {
6
+ "css": By.CSS_SELECTOR,
7
+ "xpath": By.XPATH,
8
+ "id": By.ID,
9
+ "name": By.NAME,
10
+ "tag": By.TAG_NAME,
11
+ "class": By.CLASS_NAME,
12
+ "link": By.LINK_TEXT,
13
+ "partial_link": By.PARTIAL_LINK_TEXT,
14
+ }
@@ -7,17 +7,7 @@ from selenium.webdriver.common.by import By
7
7
  from selenium.webdriver.support.ui import WebDriverWait
8
8
  from selenium.webdriver.support import expected_conditions as EC
9
9
  from mcp.types import Tool
10
-
11
- BY_MAP = {
12
- "css": By.CSS_SELECTOR,
13
- "xpath": By.XPATH,
14
- "id": By.ID,
15
- "name": By.NAME,
16
- "tag": By.TAG_NAME,
17
- "class": By.CLASS_NAME,
18
- "link": By.LINK_TEXT,
19
- "partial_link": By.PARTIAL_LINK_TEXT,
20
- }
10
+ from selenium_mcp.tools._locators import BY_MAP
21
11
 
22
12
 
23
13
  class AssertionTools:
@@ -174,7 +164,7 @@ class AssertionTools:
174
164
  async def _assert_url(self, args: dict) -> str:
175
165
  actual = self._driver().current_url
176
166
  exp = args["expected"]
177
- ok = (actual == exp) if args.get("exact", False) else (exp in actual)
167
+ ok = (actual == exp) if args.get("exact", False) else (exp.lower() in actual.lower())
178
168
  return self._pass(f"url='{actual}'") if ok else self._fail(f"expected='{exp}' | actual='{actual}'")
179
169
 
180
170
  async def _assert_text(self, args: dict) -> str:
@@ -27,7 +27,8 @@ class BrowserTools:
27
27
  opts.add_argument("--no-sandbox")
28
28
  opts.add_argument("--disable-dev-shm-usage")
29
29
  self.driver = webdriver.Chrome(options=opts)
30
- self._session_log = []
30
+ # Do NOT reset _session_log here — only explicit start_browser should do that.
31
+ # Resetting here would wipe recorded actions if the browser crashes mid-session.
31
32
  self.record("start_browser", browser="chrome", headless=False)
32
33
  return self.driver
33
34
 
@@ -165,9 +166,9 @@ class BrowserTools:
165
166
  headless = args.get("headless", False)
166
167
  try:
167
168
  w, h = args.get("window_size", "1920x1080").lower().replace(" ", "").split("x")
168
- int(w), int(h)
169
+ w, h = int(w), int(h)
169
170
  except (ValueError, AttributeError):
170
- w, h = "1920", "1080"
171
+ w, h = 1920, 1080
171
172
 
172
173
  if browser == "chrome":
173
174
  opts = ChromeOptions()
@@ -3,6 +3,8 @@ Codegen Tools — generate Python (pytest) and Java (TestNG/JUnit5) test scripts
3
3
  from the recorded session log. This is the key differentiator for Java users.
4
4
  """
5
5
 
6
+ import re
7
+ from urllib.parse import urlparse
6
8
  from mcp.types import Tool
7
9
 
8
10
 
@@ -477,14 +479,12 @@ public class {test_name} {{
477
479
 
478
480
  def _url_to_page_name(self, url: str) -> str:
479
481
  try:
480
- from urllib.parse import urlparse
481
482
  segment = urlparse(url).path.strip("/").split("/")[-1]
482
483
  return segment.capitalize() if segment else "Home"
483
484
  except Exception:
484
485
  return "Recorded"
485
486
 
486
487
  def _selector_to_name(self, selector: str, by: str) -> str:
487
- import re
488
488
  sel = selector.strip()
489
489
  if by in ("id", "name"):
490
490
  return self._to_camel(sel)
@@ -516,7 +516,6 @@ public class {test_name} {{
516
516
  return "element"
517
517
 
518
518
  def _to_camel(self, s: str) -> str:
519
- import re
520
519
  parts = [p for p in re.split(r"[_\-\s]+", s.strip()) if p]
521
520
  return (parts[0].lower() + "".join(p.capitalize() for p in parts[1:])) if parts else "element"
522
521
 
@@ -629,6 +628,14 @@ public class {test_name} {{
629
628
  f"{i}}}",
630
629
  "",
631
630
  ]
631
+ if "right_click" in acts:
632
+ lines += [
633
+ f"{i}public {page_name} rightClick{cap}() {{",
634
+ f"{i}{i}new Actions(driver).contextClick(wait.until(ExpectedConditions.visibilityOfElementLocated({name}))).perform();",
635
+ f"{i}{i}return this;",
636
+ f"{i}}}",
637
+ "",
638
+ ]
632
639
  if "scroll_to_element" in acts:
633
640
  lines += [
634
641
  f"{i}public {page_name} scrollTo{cap}() {{",
@@ -772,7 +779,6 @@ public class {test_name} {{
772
779
  )
773
780
 
774
781
  def _name_to_readable(self, name: str) -> str:
775
- import re
776
782
  return re.sub(r'([A-Z])', r' \1', name).strip().lower()
777
783
 
778
784
  def _gherkin_step_text(self, entry: dict, key_to_name: dict, prefix: str) -> str:
@@ -918,6 +924,15 @@ public class {test_name} {{
918
924
  f'{i}{i}driver.navigate().back();\n'
919
925
  f'{i}}}'
920
926
  )
927
+ elif action == "go_forward":
928
+ if "go_forward" not in seen:
929
+ seen.add("go_forward")
930
+ methods.append(
931
+ f'{i}@And("I navigate forward")\n'
932
+ f'{i}public void iNavigateForward() {{\n'
933
+ f'{i}{i}driver.navigate().forward();\n'
934
+ f'{i}}}'
935
+ )
921
936
  elif action == "refresh":
922
937
  if "refresh" not in seen:
923
938
  seen.add("refresh")
@@ -7,18 +7,7 @@ from selenium.webdriver.common.action_chains import ActionChains
7
7
  from selenium.webdriver.support.ui import WebDriverWait, Select
8
8
  from selenium.webdriver.support import expected_conditions as EC
9
9
  from mcp.types import Tool
10
-
11
-
12
- BY_MAP = {
13
- "css": By.CSS_SELECTOR,
14
- "xpath": By.XPATH,
15
- "id": By.ID,
16
- "name": By.NAME,
17
- "tag": By.TAG_NAME,
18
- "class": By.CLASS_NAME,
19
- "link": By.LINK_TEXT,
20
- "partial_link": By.PARTIAL_LINK_TEXT,
21
- }
10
+ from selenium_mcp.tools._locators import BY_MAP
22
11
 
23
12
  LOCATOR_SCHEMA = {
24
13
  "selector": {
@@ -273,7 +262,9 @@ class ElementTools:
273
262
  if args.get("clear_first", True):
274
263
  el.clear()
275
264
  el.send_keys(args["text"])
276
- self.browser.record("type_text", selector=args["selector"], by=args.get("by", "css"), text=args["text"])
265
+ sel_lower = args["selector"].lower()
266
+ logged_text = "***" if any(k in sel_lower for k in ("password", "passwd", "pwd")) else args["text"]
267
+ self.browser.record("type_text", selector=args["selector"], by=args.get("by", "css"), text=logged_text)
277
268
  return f"✅ Typed into '{args['selector']}'"
278
269
 
279
270
  async def _get_text(self, args: dict) -> str:
@@ -323,8 +314,9 @@ class ElementTools:
323
314
  async def _drag_and_drop(self, args: dict) -> str:
324
315
  by = self._by(args.get("by", "css"))
325
316
  driver = self.browser.get_driver()
326
- src = driver.find_element(by, args["source_selector"])
327
- tgt = driver.find_element(by, args["target_selector"])
317
+ timeout = args.get("timeout", 10)
318
+ src = WebDriverWait(driver, timeout).until(EC.presence_of_element_located((by, args["source_selector"])))
319
+ tgt = WebDriverWait(driver, timeout).until(EC.presence_of_element_located((by, args["target_selector"])))
328
320
  ActionChains(driver).drag_and_drop(src, tgt).perform()
329
321
  return f"✅ Dragged '{args['source_selector']}' → '{args['target_selector']}'"
330
322
 
@@ -1,8 +1,8 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: seleniumboot-mcp
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: A Python MCP server for Selenium WebDriver — browser automation, Java TestNG/JUnit5/Cucumber codegen, and Page Object Model generation
5
- Author-email: Panjatan <panjatan.tech@gmail.com>
5
+ Author-email: Raza Tech <razatechnologyservices@gmail.com>
6
6
  License: MIT
7
7
  Project-URL: Homepage, https://github.com/seleniumboot/selenium-mcp
8
8
  Project-URL: Repository, https://github.com/seleniumboot/selenium-mcp
@@ -25,6 +25,7 @@ Requires-Dist: selenium>=4.6.0
25
25
  A Python **Model Context Protocol (MCP)** server for Selenium WebDriver automation.
26
26
  Let Claude or GitHub Copilot control a real browser — navigate pages, interact with elements,
27
27
  run assertions, and generate ready-to-run **Java TestNG / JUnit 5 / Cucumber / pytest** test code from recorded sessions.
28
+ 43 tools. No ChromeDriver setup. Browser auto-starts on first use.
28
29
 
29
30
  [![PyPI](https://img.shields.io/pypi/v/seleniumboot-mcp)](https://pypi.org/project/seleniumboot-mcp/)
30
31
  [![Python](https://img.shields.io/pypi/pyversions/seleniumboot-mcp)](https://pypi.org/project/seleniumboot-mcp/)
@@ -106,7 +107,7 @@ Claude controls the real browser, records every action, and on request generates
106
107
 
107
108
  ---
108
109
 
109
- ## Tools (39 total)
110
+ ## Tools (43 total)
110
111
 
111
112
  ### Browser
112
113
  | Tool | Description |
@@ -3,6 +3,7 @@ pyproject.toml
3
3
  src/selenium_mcp/__init__.py
4
4
  src/selenium_mcp/server.py
5
5
  src/selenium_mcp/tools/__init__.py
6
+ src/selenium_mcp/tools/_locators.py
6
7
  src/selenium_mcp/tools/assertion_tools.py
7
8
  src/selenium_mcp/tools/browser_tools.py
8
9
  src/selenium_mcp/tools/codegen_tools.py
@@ -12,8 +13,4 @@ src/seleniumboot_mcp.egg-info/SOURCES.txt
12
13
  src/seleniumboot_mcp.egg-info/dependency_links.txt
13
14
  src/seleniumboot_mcp.egg-info/entry_points.txt
14
15
  src/seleniumboot_mcp.egg-info/requires.txt
15
- src/seleniumboot_mcp.egg-info/top_level.txt
16
- src/tools/assertion_tools.py
17
- src/tools/browser_tools.py
18
- src/tools/codegen_tools.py
19
- src/tools/element_tools.py
16
+ src/seleniumboot_mcp.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ seleniumboot-mcp = selenium_mcp.server:run
@@ -1,245 +0,0 @@
1
- """
2
- Assertion Tools — validate page state, element state, text, URL, title
3
- Returns PASS/FAIL with detail — useful for AI-driven test verification.
4
- """
5
-
6
- from selenium.webdriver.common.by import By
7
- from selenium.webdriver.support.ui import WebDriverWait
8
- from selenium.webdriver.support import expected_conditions as EC
9
- from mcp.types import Tool
10
-
11
- BY_MAP = {
12
- "css": By.CSS_SELECTOR,
13
- "xpath": By.XPATH,
14
- "id": By.ID,
15
- "name": By.NAME,
16
- "tag": By.TAG_NAME,
17
- "class": By.CLASS_NAME,
18
- "link": By.LINK_TEXT,
19
- "partial_link": By.PARTIAL_LINK_TEXT,
20
- }
21
-
22
-
23
- class AssertionTools:
24
- def __init__(self, browser_tools):
25
- self.browser = browser_tools
26
-
27
- def _driver(self):
28
- return self.browser.get_driver()
29
-
30
- def _by(self, strategy: str):
31
- return BY_MAP.get(strategy, By.CSS_SELECTOR)
32
-
33
- def get_tools(self) -> list[Tool]:
34
- return [
35
- Tool(
36
- name="assert_title",
37
- description="Assert the page title equals or contains expected text.",
38
- inputSchema={
39
- "type": "object",
40
- "properties": {
41
- "expected": {"type": "string"},
42
- "exact": {"type": "boolean", "default": False}
43
- },
44
- "required": ["expected"],
45
- },
46
- ),
47
- Tool(
48
- name="assert_url",
49
- description="Assert the current URL equals or contains expected value.",
50
- inputSchema={
51
- "type": "object",
52
- "properties": {
53
- "expected": {"type": "string"},
54
- "exact": {"type": "boolean", "default": False}
55
- },
56
- "required": ["expected"],
57
- },
58
- ),
59
- Tool(
60
- name="assert_text",
61
- description="Assert element text equals or contains expected value.",
62
- inputSchema={
63
- "type": "object",
64
- "properties": {
65
- "selector": {"type": "string"},
66
- "by": {"type": "string", "default": "css"},
67
- "expected": {"type": "string"},
68
- "exact": {"type": "boolean", "default": False},
69
- "timeout": {"type": "integer", "default": 10}
70
- },
71
- "required": ["selector", "expected"],
72
- },
73
- ),
74
- Tool(
75
- name="assert_element_visible",
76
- description="Assert an element is visible on the page.",
77
- inputSchema={
78
- "type": "object",
79
- "properties": {
80
- "selector": {"type": "string"},
81
- "by": {"type": "string", "default": "css"},
82
- "timeout": {"type": "integer", "default": 10}
83
- },
84
- "required": ["selector"],
85
- },
86
- ),
87
- Tool(
88
- name="assert_element_not_visible",
89
- description="Assert an element is NOT visible (hidden or absent).",
90
- inputSchema={
91
- "type": "object",
92
- "properties": {
93
- "selector": {"type": "string"},
94
- "by": {"type": "string", "default": "css"},
95
- "timeout": {"type": "integer", "default": 3}
96
- },
97
- "required": ["selector"],
98
- },
99
- ),
100
- Tool(
101
- name="assert_attribute",
102
- description="Assert an element attribute has the expected value.",
103
- inputSchema={
104
- "type": "object",
105
- "properties": {
106
- "selector": {"type": "string"},
107
- "by": {"type": "string", "default": "css"},
108
- "attribute": {"type": "string"},
109
- "expected": {"type": "string"},
110
- "exact": {"type": "boolean", "default": True},
111
- "timeout": {"type": "integer", "default": 10}
112
- },
113
- "required": ["selector", "attribute", "expected"],
114
- },
115
- ),
116
- Tool(
117
- name="assert_page_contains",
118
- description="Assert the full page source or visible text contains a string.",
119
- inputSchema={
120
- "type": "object",
121
- "properties": {
122
- "text": {"type": "string"},
123
- "source_only": {
124
- "type": "boolean",
125
- "default": False,
126
- "description": "If true, checks page source; otherwise checks body visible text"
127
- }
128
- },
129
- "required": ["text"],
130
- },
131
- ),
132
- Tool(
133
- name="assert_element_count",
134
- description="Assert number of elements matching selector equals expected count.",
135
- inputSchema={
136
- "type": "object",
137
- "properties": {
138
- "selector": {"type": "string"},
139
- "by": {"type": "string", "default": "css"},
140
- "expected_count": {"type": "integer"}
141
- },
142
- "required": ["selector", "expected_count"],
143
- },
144
- ),
145
- ]
146
-
147
- def get_handlers(self) -> dict:
148
- return {
149
- "assert_title": self._assert_title,
150
- "assert_url": self._assert_url,
151
- "assert_text": self._assert_text,
152
- "assert_element_visible": self._assert_element_visible,
153
- "assert_element_not_visible": self._assert_element_not_visible,
154
- "assert_attribute": self._assert_attribute,
155
- "assert_page_contains": self._assert_page_contains,
156
- "assert_element_count": self._assert_element_count,
157
- }
158
-
159
- # ------------------------------------------------------------------ #
160
- # Helpers #
161
- # ------------------------------------------------------------------ #
162
- def _pass(self, msg): return f"✅ PASS | {msg}"
163
- def _fail(self, msg): return f"❌ FAIL | {msg}"
164
-
165
- # ------------------------------------------------------------------ #
166
- # Handlers #
167
- # ------------------------------------------------------------------ #
168
- async def _assert_title(self, args: dict) -> str:
169
- actual = self._driver().title
170
- exp = args["expected"]
171
- ok = (actual == exp) if args.get("exact", False) else (exp.lower() in actual.lower())
172
- return self._pass(f"title='{actual}'") if ok else self._fail(f"expected='{exp}' | actual='{actual}'")
173
-
174
- async def _assert_url(self, args: dict) -> str:
175
- actual = self._driver().current_url
176
- exp = args["expected"]
177
- ok = (actual == exp) if args.get("exact", False) else (exp in actual)
178
- return self._pass(f"url='{actual}'") if ok else self._fail(f"expected='{exp}' | actual='{actual}'")
179
-
180
- async def _assert_text(self, args: dict) -> str:
181
- by = self._by(args.get("by", "css"))
182
- timeout = args.get("timeout", 10)
183
- try:
184
- el = WebDriverWait(self._driver(), timeout).until(
185
- EC.visibility_of_element_located((by, args["selector"]))
186
- )
187
- except Exception:
188
- return self._fail(f"element '{args['selector']}' not found within {timeout}s")
189
- actual = el.text
190
- exp = args["expected"]
191
- ok = (actual == exp) if args.get("exact", False) else (exp.lower() in actual.lower())
192
- return self._pass(f"text='{actual}'") if ok else self._fail(f"expected='{exp}' | actual='{actual}'")
193
-
194
- async def _assert_element_visible(self, args: dict) -> str:
195
- by = self._by(args.get("by", "css"))
196
- timeout = args.get("timeout", 10)
197
- try:
198
- WebDriverWait(self._driver(), timeout).until(
199
- EC.visibility_of_element_located((by, args["selector"]))
200
- )
201
- return self._pass(f"'{args['selector']}' is visible")
202
- except Exception:
203
- return self._fail(f"'{args['selector']}' is NOT visible")
204
-
205
- async def _assert_element_not_visible(self, args: dict) -> str:
206
- by = self._by(args.get("by", "css"))
207
- timeout = args.get("timeout", 3)
208
- try:
209
- WebDriverWait(self._driver(), timeout).until(
210
- EC.invisibility_of_element_located((by, args["selector"]))
211
- )
212
- return self._pass(f"'{args['selector']}' is not visible")
213
- except Exception:
214
- return self._fail(f"'{args['selector']}' IS visible (expected hidden)")
215
-
216
- async def _assert_attribute(self, args: dict) -> str:
217
- by = self._by(args.get("by", "css"))
218
- timeout = args.get("timeout", 10)
219
- try:
220
- el = WebDriverWait(self._driver(), timeout).until(
221
- EC.presence_of_element_located((by, args["selector"]))
222
- )
223
- except Exception:
224
- return self._fail(f"element '{args['selector']}' not found within {timeout}s")
225
- actual = el.get_attribute(args["attribute"])
226
- exp = args["expected"]
227
- ok = (actual == exp) if args.get("exact", True) else (exp in (actual or ""))
228
- return self._pass(f"{args['attribute']}='{actual}'") if ok else self._fail(f"expected='{exp}' | actual='{actual}'")
229
-
230
- async def _assert_page_contains(self, args: dict) -> str:
231
- text = args["text"]
232
- if args.get("source_only", False):
233
- content = self._driver().page_source
234
- else:
235
- content = self._driver().find_element(By.TAG_NAME, "body").text
236
- ok = text.lower() in content.lower()
237
- return self._pass(f"page contains '{text}'") if ok else self._fail(f"'{text}' not found on page")
238
-
239
- async def _assert_element_count(self, args: dict) -> str:
240
- by = self._by(args.get("by", "css"))
241
- els = self._driver().find_elements(by, args["selector"])
242
- actual = len(els)
243
- exp = args["expected_count"]
244
- ok = actual == exp
245
- return self._pass(f"count={actual}") if ok else self._fail(f"expected={exp} | actual={actual}")