mirror of
https://github.com/Skyvern-AI/skyvern.git
synced 2026-04-28 11:40:32 +00:00
113 lines
4.4 KiB
Python
113 lines
4.4 KiB
Python
"""Tests for compute_stable_selector and _looks_dynamic."""
|
|
|
|
from skyvern.utils.css_selector import _looks_dynamic, compute_stable_selector
|
|
|
|
|
|
class TestLooksDynamic:
|
|
"""Tests for the _looks_dynamic heuristic."""
|
|
|
|
def test_long_hex_ids_are_dynamic(self):
|
|
assert _looks_dynamic("ember12345678") is True
|
|
assert _looks_dynamic("react-abcdef01") is True
|
|
|
|
def test_word_digit_ids_are_dynamic(self):
|
|
assert _looks_dynamic("uid-12345") is True
|
|
assert _looks_dynamic("el_56789") is True
|
|
|
|
def test_semantic_ids_are_not_dynamic(self):
|
|
"""Meaningful IDs should NOT be flagged as dynamic."""
|
|
assert _looks_dynamic("username") is False
|
|
assert _looks_dynamic("sign_in") is False
|
|
assert _looks_dynamic("passwd") is False
|
|
assert _looks_dynamic("inputUsername") is False
|
|
assert _looks_dynamic("login-form") is False
|
|
assert _looks_dynamic("signOnButton") is False
|
|
|
|
def test_single_char_ids_not_dynamic(self):
|
|
"""Very short IDs shouldn't match the all-caps pattern."""
|
|
assert _looks_dynamic("A") is False
|
|
assert _looks_dynamic("AB") is False
|
|
|
|
def test_title_case_ids_not_dynamic(self):
|
|
"""Title-case or mixed-case IDs are likely meaningful, not generated."""
|
|
assert _looks_dynamic("Login") is False
|
|
assert _looks_dynamic("Form") is False
|
|
assert _looks_dynamic("Nav") is False
|
|
assert _looks_dynamic("Input") is False
|
|
assert _looks_dynamic("Button") is False
|
|
|
|
|
|
class TestComputeStableSelector:
|
|
"""Tests for compute_stable_selector priority and output."""
|
|
|
|
def test_prefers_text_over_dynamic_id(self):
|
|
"""When ID is dynamic and text is available, use :has-text()."""
|
|
elem = {
|
|
"tagName": "button",
|
|
"text": "Apply Filters",
|
|
"attributes": {"id": "ember12345678", "type": "submit"},
|
|
}
|
|
result = compute_stable_selector(elem)
|
|
assert result == 'button:has-text("Apply Filters")'
|
|
|
|
def test_static_id_preferred_over_text(self):
|
|
"""When ID looks stable, use it (it's higher priority)."""
|
|
elem = {
|
|
"tagName": "button",
|
|
"text": "Submit",
|
|
"attributes": {"id": "signOnButton"},
|
|
}
|
|
result = compute_stable_selector(elem)
|
|
assert result == "#signOnButton"
|
|
|
|
def test_aria_label_preferred_over_text(self):
|
|
elem = {
|
|
"tagName": "input",
|
|
"text": "",
|
|
"attributes": {"aria-label": "Email Address", "id": "uid-12345"},
|
|
}
|
|
result = compute_stable_selector(elem)
|
|
assert result == 'input[aria-label="Email Address"]'
|
|
|
|
def test_realistic_scraper_shape(self):
|
|
"""Scraper output shape (from domUtils.js buildElementObject) should work directly.
|
|
|
|
el_data has top-level 'id' (Skyvern uniqueId), 'tagName', 'text',
|
|
and nested 'attributes' with real HTML attrs. The scraper stores
|
|
unique_id as a separate attribute, not as 'id'.
|
|
"""
|
|
el_data = {
|
|
"id": "AAGD", # Skyvern unique_id — NOT the HTML id
|
|
"tagName": "button",
|
|
"attributes": {"unique_id": "AAGD", "type": "submit"},
|
|
"text": "Apply Filters",
|
|
"xpath": "/html/body/div/button",
|
|
}
|
|
result = compute_stable_selector(el_data)
|
|
assert result == 'button:has-text("Apply Filters")'
|
|
|
|
def test_realistic_scraper_shape_with_aria_label(self):
|
|
"""Scraper shape with aria-label should produce aria-label selector."""
|
|
el_data = {
|
|
"id": "AABC",
|
|
"tagName": "input",
|
|
"attributes": {"unique_id": "AABC", "aria-label": "Email Address", "type": "email"},
|
|
"text": "",
|
|
}
|
|
result = compute_stable_selector(el_data)
|
|
assert result == 'input[aria-label="Email Address"]'
|
|
|
|
def test_realistic_scraper_shape_with_stable_html_id(self):
|
|
"""When the HTML id is stable (same as Skyvern id only by coincidence), use it."""
|
|
el_data = {
|
|
"id": "AAGD",
|
|
"tagName": "button",
|
|
"attributes": {"id": "signOnButton", "type": "submit"},
|
|
"text": "Sign On",
|
|
}
|
|
result = compute_stable_selector(el_data)
|
|
assert result == "#signOnButton"
|
|
|
|
def test_no_data_returns_none(self):
|
|
assert compute_stable_selector(None) is None
|
|
assert compute_stable_selector({}) is None
|