From cec5acd68a3c73791342f3c3e08d4f7bd58c7b06 Mon Sep 17 00:00:00 2001
From: awanawona <>
Date: Mon, 16 Mar 2026 18:01:11 +0800
Subject: [PATCH] test: add edge case tests for filter, iterancestors, and
find_similar
- test_selectors_filter.py: covers chained filter(), empty result,
all-pass predicate, and calling filter() on empty Selectors
- test_ancestor_navigation.py: covers iterancestors() order, depth,
text-node safety, root-element edge case, and find_ancestor() with
no match
- test_find_similar_advanced.py: covers similarity_threshold levels,
match_text=True behavior, ignore_attributes combinations, and
text-node safety
---
tests/parser/test_ancestor_navigation.py | 66 ++++++++++++++++++
tests/parser/test_find_similar_advanced.py | 78 ++++++++++++++++++++++
tests/parser/test_selectors_filter.py | 64 ++++++++++++++++++
3 files changed, 208 insertions(+)
create mode 100644 tests/parser/test_ancestor_navigation.py
create mode 100644 tests/parser/test_find_similar_advanced.py
create mode 100644 tests/parser/test_selectors_filter.py
diff --git a/tests/parser/test_ancestor_navigation.py b/tests/parser/test_ancestor_navigation.py
new file mode 100644
index 0000000..1814ec5
--- /dev/null
+++ b/tests/parser/test_ancestor_navigation.py
@@ -0,0 +1,66 @@
+"""
+Tests for Selector.iterancestors() and Selector.find_ancestor() methods.
+Target file: tests/parser/test_general.py (append to TestElementNavigation class)
+"""
+import pytest
+from scrapling import Selector
+
+
+@pytest.fixture
+def nested_page():
+ html = """
+
+
+
+ """
+ return Selector(html, adaptive=False)
+
+
+class TestAncestorNavigation:
+ def test_iterancestors_returns_all_ancestors(self, nested_page):
+ """iterancestors() should yield every ancestor up to """
+ target = nested_page.css("#target")[0]
+ ancestor_tags = [a.tag for a in target.iterancestors()]
+ # Expected order: p → article → section → div → body → html
+ assert ancestor_tags[:4] == ["p", "article", "section", "div"]
+ assert "body" in ancestor_tags
+ assert "html" in ancestor_tags
+
+ def test_iterancestors_order_is_bottom_up(self, nested_page):
+ """iterancestors() should start from the immediate parent, not the root"""
+ target = nested_page.css("#target")[0]
+ first_ancestor = next(target.iterancestors())
+ assert first_ancestor.attrib.get("id") == "level4"
+
+ def test_find_ancestor_returns_first_match(self, nested_page):
+ """find_ancestor() should return the closest ancestor matching the predicate"""
+ target = nested_page.css("#target")[0]
+ # Looking for the nearest ancestor with class "card"
+ result = target.find_ancestor(lambda el: el.has_class("card"))
+ assert result is not None
+ assert result.attrib.get("id") == "level3"
+
+ def test_find_ancestor_returns_none_when_not_found(self, nested_page):
+ """find_ancestor() should return None if no ancestor matches"""
+ target = nested_page.css("#target")[0]
+ result = target.find_ancestor(lambda el: el.has_class("nonexistent-class"))
+ assert result is None
+
+ def test_iterancestors_on_text_node_is_empty(self, nested_page):
+ """iterancestors() on a text node should yield nothing (not raise)"""
+ text_node = nested_page.css("#target::text")[0]
+ ancestors = list(text_node.iterancestors())
+ assert ancestors == []
+
+ def test_find_ancestor_on_root_element_returns_none(self, nested_page):
+ """find_ancestor() on the root element should return None gracefully"""
+ # html element has no ancestors
+ html_el = nested_page.css("html")[0]
+ result = html_el.find_ancestor(lambda el: True)
+ assert result is None
diff --git a/tests/parser/test_find_similar_advanced.py b/tests/parser/test_find_similar_advanced.py
new file mode 100644
index 0000000..480f155
--- /dev/null
+++ b/tests/parser/test_find_similar_advanced.py
@@ -0,0 +1,78 @@
+"""
+Tests for Selector.find_similar() with non-default parameters.
+Target file: tests/parser/test_general.py (append to TestSimilarElements class)
+"""
+import pytest
+from scrapling import Selector
+
+
+@pytest.fixture
+def product_page():
+ html = """
+
+
+
+ Apple
+
+
+ Banana
+
+
+ Carrot
+
+
+
+
+
+ """
+ return Selector(html, adaptive=False)
+
+
+class TestFindSimilarAdvanced:
+ def test_find_similar_default_finds_same_tag_siblings(self, product_page):
+ """find_similar() with defaults should find div.product siblings, not the section"""
+ first = product_page.css("div.product")[0]
+ similar = first.find_similar()
+ tags = [el.tag for el in similar]
+ assert all(t == "div" for t in tags), "Should only return elements"
+ assert len(similar) == 2 # Banana and Carrot, not Grape (section)
+
+ def test_find_similar_high_threshold_filters_more(self, product_page):
+ """A higher similarity_threshold should return fewer (or equal) results"""
+ first = product_page.css("div.product")[0]
+ low_threshold = first.find_similar(similarity_threshold=0.1)
+ high_threshold = first.find_similar(similarity_threshold=0.9)
+ assert len(high_threshold) <= len(low_threshold)
+
+ def test_find_similar_match_text_excludes_different_text(self, product_page):
+ """match_text=True should factor in text content during similarity scoring"""
+ first = product_page.css("div.product")[0] # Apple
+ # With match_text=True and a high threshold, "Apple" vs "Banana"/"Carrot" text
+ # should reduce similarity scores — result count may drop
+ with_text = first.find_similar(similarity_threshold=0.8, match_text=True)
+ without_text = first.find_similar(similarity_threshold=0.8, match_text=False)
+ # match_text=True is stricter when text differs, so result should be <= without_text
+ assert len(with_text) <= len(without_text)
+
+ def test_find_similar_ignore_attributes_affects_matching(self, product_page):
+ """Ignoring data-price should make more elements qualify as similar"""
+ first = product_page.css("div.product")[0]
+ # Ignore both data-price and data-category → only class matters → all 3 divs match
+ ignore_all_data = first.find_similar(
+ similarity_threshold=0.2,
+ ignore_attributes=["data-price", "data-category"]
+ )
+ # Ignore nothing → data-category difference (fruit vs veggie) may reduce matches
+ ignore_nothing = first.find_similar(
+ similarity_threshold=0.9,
+ ignore_attributes=[]
+ )
+ assert len(ignore_all_data) >= len(ignore_nothing)
+
+ def test_find_similar_on_text_node_returns_empty(self, product_page):
+ """find_similar() on a text node should return empty Selectors without raising"""
+ text_node = product_page.css(".name::text")[0]
+ result = text_node.find_similar()
+ assert len(result) == 0
diff --git a/tests/parser/test_selectors_filter.py b/tests/parser/test_selectors_filter.py
new file mode 100644
index 0000000..9612abc
--- /dev/null
+++ b/tests/parser/test_selectors_filter.py
@@ -0,0 +1,64 @@
+"""
+Tests for Selectors.filter() method edge cases.
+Target file: tests/parser/test_parser_advanced.py (append to TestAdvancedSelectors class)
+"""
+import pytest
+from scrapling import Selector, Selectors
+
+
+@pytest.fixture
+def page():
+ html = """
+
+
+ - Apple
+ - Banana
+ - Cherry
+ - Durian
+
+
+ """
+ return Selector(html, adaptive=False)
+
+
+class TestSelectorsFilter:
+ def test_filter_basic(self, page):
+ """filter() should return only elements matching the predicate"""
+ items = page.css("li.item")
+ expensive = items.filter(lambda el: int(el.attrib.get("data-value", 0)) >= 10)
+ assert len(expensive) == 2
+ texts = expensive.getall()
+ assert any("Apple" in t for t in texts)
+ assert any("Cherry" in t for t in texts)
+
+ def test_filter_returns_empty_selectors_when_no_match(self, page):
+ """filter() should return an empty Selectors (not None/exception) when nothing matches"""
+ items = page.css("li.item")
+ result = items.filter(lambda el: int(el.attrib.get("data-value", 0)) > 9999)
+ assert isinstance(result, Selectors)
+ assert len(result) == 0
+ assert result.first is None
+
+ def test_filter_all_pass(self, page):
+ """filter() with always-True predicate should return all elements"""
+ items = page.css("li.item")
+ result = items.filter(lambda el: True)
+ assert len(result) == len(items)
+
+ def test_filter_chained(self, page):
+ """filter() should be chainable — apply two filters in sequence"""
+ items = page.css("li.item")
+ # First: value > 0, then: not disabled
+ result = (
+ items
+ .filter(lambda el: int(el.attrib.get("data-value", 0)) > 0)
+ .filter(lambda el: not el.has_class("disabled"))
+ )
+ assert len(result) == 3 # Apple, Banana, Cherry (Durian is disabled AND value=0)
+
+ def test_filter_on_empty_selectors(self):
+ """filter() on an already-empty Selectors should not raise"""
+ empty = Selectors()
+ result = empty.filter(lambda el: True)
+ assert isinstance(result, Selectors)
+ assert len(result) == 0