seleniumboot-mcp 0.2.1__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.
- {seleniumboot_mcp-0.2.1 → seleniumboot_mcp-0.2.2}/PKG-INFO +2 -2
- {seleniumboot_mcp-0.2.1 → seleniumboot_mcp-0.2.2}/pyproject.toml +2 -2
- {seleniumboot_mcp-0.2.1 → seleniumboot_mcp-0.2.2}/src/selenium_mcp/server.py +1 -0
- seleniumboot_mcp-0.2.2/src/selenium_mcp/tools/_locators.py +14 -0
- {seleniumboot_mcp-0.2.1/src → seleniumboot_mcp-0.2.2/src/selenium_mcp}/tools/assertion_tools.py +2 -12
- {seleniumboot_mcp-0.2.1/src → seleniumboot_mcp-0.2.2/src/selenium_mcp}/tools/browser_tools.py +4 -3
- {seleniumboot_mcp-0.2.1 → seleniumboot_mcp-0.2.2}/src/selenium_mcp/tools/codegen_tools.py +19 -4
- {seleniumboot_mcp-0.2.1/src → seleniumboot_mcp-0.2.2/src/selenium_mcp}/tools/element_tools.py +7 -15
- {seleniumboot_mcp-0.2.1 → seleniumboot_mcp-0.2.2}/src/seleniumboot_mcp.egg-info/PKG-INFO +2 -2
- {seleniumboot_mcp-0.2.1 → seleniumboot_mcp-0.2.2}/src/seleniumboot_mcp.egg-info/SOURCES.txt +2 -5
- {seleniumboot_mcp-0.2.1 → seleniumboot_mcp-0.2.2}/src/seleniumboot_mcp.egg-info/top_level.txt +0 -1
- seleniumboot_mcp-0.2.1/src/selenium_mcp/tools/assertion_tools.py +0 -245
- seleniumboot_mcp-0.2.1/src/selenium_mcp/tools/browser_tools.py +0 -245
- seleniumboot_mcp-0.2.1/src/selenium_mcp/tools/element_tools.py +0 -364
- seleniumboot_mcp-0.2.1/src/tools/codegen_tools.py +0 -970
- {seleniumboot_mcp-0.2.1 → seleniumboot_mcp-0.2.2}/README.md +0 -0
- {seleniumboot_mcp-0.2.1 → seleniumboot_mcp-0.2.2}/setup.cfg +0 -0
- {seleniumboot_mcp-0.2.1 → seleniumboot_mcp-0.2.2}/src/selenium_mcp/__init__.py +0 -0
- {seleniumboot_mcp-0.2.1 → seleniumboot_mcp-0.2.2}/src/selenium_mcp/tools/__init__.py +0 -0
- {seleniumboot_mcp-0.2.1 → seleniumboot_mcp-0.2.2}/src/seleniumboot_mcp.egg-info/dependency_links.txt +0 -0
- {seleniumboot_mcp-0.2.1 → seleniumboot_mcp-0.2.2}/src/seleniumboot_mcp.egg-info/entry_points.txt +0 -0
- {seleniumboot_mcp-0.2.1 → 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.
|
|
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:
|
|
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
|
|
@@ -4,12 +4,12 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "seleniumboot-mcp"
|
|
7
|
-
version = "0.2.
|
|
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 = "
|
|
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",
|
|
@@ -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,
|
|
@@ -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
|
+
}
|
{seleniumboot_mcp-0.2.1/src → seleniumboot_mcp-0.2.2/src/selenium_mcp}/tools/assertion_tools.py
RENAMED
|
@@ -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:
|
{seleniumboot_mcp-0.2.1/src → seleniumboot_mcp-0.2.2/src/selenium_mcp}/tools/browser_tools.py
RENAMED
|
@@ -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
|
-
|
|
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 =
|
|
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")
|
{seleniumboot_mcp-0.2.1/src → seleniumboot_mcp-0.2.2/src/selenium_mcp}/tools/element_tools.py
RENAMED
|
@@ -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
|
-
|
|
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
|
-
|
|
327
|
-
|
|
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.
|
|
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:
|
|
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
|
|
@@ -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
|
|
@@ -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}")
|