Skyvern/tests/unit/test_css_selector.py

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