aboutsummaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
authorLouis Burda <dev@sinitax.com>2026-02-28 18:54:19 +0100
committerLouis Burda <dev@sinitax.com>2026-02-28 18:54:19 +0100
commitbe1dd21f8e4fbd5361531b4d8727a0d0d243e8ec (patch)
treee7b540012e0510d1304d2dac8e137545ae103f75 /tests
parentd70a199a72bf9a69eb4a3fcf166b0435918b2e33 (diff)
downloadselectui-main.tar.gz
selectui-main.zip
Add tests and justfileHEADmain
Diffstat (limited to 'tests')
-rw-r--r--tests/conftest.py34
-rw-r--r--tests/test_basic.py13
-rw-r--r--tests/test_filtering.py318
-rw-r--r--tests/test_integration.py286
-rw-r--r--tests/test_models.py240
-rw-r--r--tests/test_selectui.py272
6 files changed, 1157 insertions, 6 deletions
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..3d0af31
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,34 @@
+from typing import Any, Dict, List
+
+import pytest
+
+
+@pytest.fixture
+def sample_items() -> List[Dict[str, Any]]:
+ """Sample items for testing."""
+ return [
+ {"title": "Python", "subtitle": "Programming language", "info": "1991"},
+ {"title": "JavaScript", "subtitle": "Web language", "info": "1995"},
+ {"title": "Rust", "subtitle": "Systems language", "info": "2010"},
+ {"title": "Go", "subtitle": "Concurrent language", "info": "2009"},
+ ]
+
+
+@pytest.fixture
+def custom_key_items() -> List[Dict[str, Any]]:
+ """Items with custom key names."""
+ return [
+ {"name": "Alice", "role": "Engineer", "team": "Backend", "level": "Senior"},
+ {"name": "Bob", "role": "Designer", "team": "Product", "level": "Mid"},
+ {"name": "Charlie", "role": "Manager", "team": "Engineering", "level": "Staff"},
+ ]
+
+
+@pytest.fixture
+def single_line_items() -> List[Dict[str, Any]]:
+ """Items for single-line display (no subtitle)."""
+ return [
+ {"filename": "config.json", "size": 1024},
+ {"filename": "data.csv", "size": 4096},
+ {"filename": "README.md", "size": 2048},
+ ]
diff --git a/tests/test_basic.py b/tests/test_basic.py
index e7c95af..7e4f871 100644
--- a/tests/test_basic.py
+++ b/tests/test_basic.py
@@ -1,9 +1,10 @@
#!/usr/bin/env python3
"""Basic tests for selectui functionality."""
-from selectui import SelectUI
from thefuzz import fuzz
+from selectui import SelectUI
+
def test_filtering_logic():
"""Test the filtering logic."""
@@ -60,8 +61,8 @@ def test_ui_initialization():
ui = SelectUI(items)
assert ui.items == items
assert ui.filtered_items == items
- assert ui.fuzzy_search == False
- assert ui.case_sensitive == False
+ assert not ui.fuzzy_search
+ assert not ui.case_sensitive
print("✓ UI initialization passed")
@@ -71,10 +72,10 @@ def test_ui_initialization_empty():
ui = SelectUI()
assert ui.items == []
assert ui.filtered_items == []
- assert ui.fuzzy_search == False
- assert ui.case_sensitive == False
+ assert not ui.fuzzy_search
+ assert not ui.case_sensitive
assert ui.stdin_fd is None
- assert ui._stream_complete == False
+ assert not ui._stream_complete
print("✓ UI empty initialization passed")
diff --git a/tests/test_filtering.py b/tests/test_filtering.py
new file mode 100644
index 0000000..43a2e9b
--- /dev/null
+++ b/tests/test_filtering.py
@@ -0,0 +1,318 @@
+from thefuzz import fuzz
+
+
+class TestExactFiltering:
+ """Test exact string matching."""
+
+ def test_filter_by_title_case_insensitive(self, sample_items):
+ """Test filtering by title (case insensitive)."""
+ query = "python"
+ filtered = []
+
+ for item in sample_items:
+ title = str(item.get("title", "")).lower()
+ subtitle = str(item.get("subtitle", "")).lower()
+ if query in title or query in subtitle:
+ filtered.append(item)
+
+ assert len(filtered) == 1
+ assert filtered[0]["title"] == "Python"
+
+ def test_filter_by_subtitle(self, sample_items):
+ """Test filtering by subtitle."""
+ query = "web"
+ filtered = []
+
+ for item in sample_items:
+ title = str(item.get("title", "")).lower()
+ subtitle = str(item.get("subtitle", "")).lower()
+ if query in title or query in subtitle:
+ filtered.append(item)
+
+ assert len(filtered) == 1
+ assert filtered[0]["title"] == "JavaScript"
+
+ def test_filter_no_matches(self, sample_items):
+ """Test filtering with no matches."""
+ query = "nonexistent"
+ filtered = []
+
+ for item in sample_items:
+ title = str(item.get("title", "")).lower()
+ subtitle = str(item.get("subtitle", "")).lower()
+ if query in title or query in subtitle:
+ filtered.append(item)
+
+ assert len(filtered) == 0
+
+ def test_filter_all_match(self, sample_items):
+ """Test filtering that matches all items."""
+ query = "language"
+ filtered = []
+
+ for item in sample_items:
+ title = str(item.get("title", "")).lower()
+ subtitle = str(item.get("subtitle", "")).lower()
+ if query in title or query in subtitle:
+ filtered.append(item)
+
+ assert len(filtered) == 4
+
+ def test_filter_partial_match(self, sample_items):
+ """Test partial string matching."""
+ query = "rust"
+ filtered = []
+
+ for item in sample_items:
+ title = str(item.get("title", "")).lower()
+ subtitle = str(item.get("subtitle", "")).lower()
+ if query in title or query in subtitle:
+ filtered.append(item)
+
+ assert len(filtered) == 1
+ assert filtered[0]["title"] == "Rust"
+
+
+class TestCaseSensitiveFiltering:
+ """Test case-sensitive filtering."""
+
+ def test_case_sensitive_title_match(self, sample_items):
+ """Test case-sensitive title matching."""
+ query = "Python"
+ filtered = []
+
+ for item in sample_items:
+ title = str(item.get("title", ""))
+ subtitle = str(item.get("subtitle", ""))
+ if query in title or query in subtitle:
+ filtered.append(item)
+
+ assert len(filtered) == 1
+ assert filtered[0]["title"] == "Python"
+
+ def test_case_sensitive_no_match(self, sample_items):
+ """Test case-sensitive no match."""
+ query = "python" # lowercase
+ filtered = []
+
+ for item in sample_items:
+ title = str(item.get("title", ""))
+ subtitle = str(item.get("subtitle", ""))
+ if query in title or query in subtitle:
+ filtered.append(item)
+
+ assert len(filtered) == 0
+
+ def test_case_sensitive_subtitle_match(self, sample_items):
+ """Test case-sensitive subtitle matching."""
+ query = "Programming"
+ filtered = []
+
+ for item in sample_items:
+ title = str(item.get("title", ""))
+ subtitle = str(item.get("subtitle", ""))
+ if query in title or query in subtitle:
+ filtered.append(item)
+
+ assert len(filtered) == 1
+
+
+class TestFuzzyFiltering:
+ """Test fuzzy search functionality."""
+
+ def test_fuzzy_match_with_typo(self, sample_items):
+ """Test fuzzy matching with a typo."""
+ query = "Pythn" # Missing 'o'
+ filtered = []
+
+ for item in sample_items:
+ title = str(item.get("title", ""))
+ subtitle = str(item.get("subtitle", ""))
+ title_score = fuzz.partial_ratio(query, title)
+ subtitle_score = fuzz.partial_ratio(query, subtitle)
+ if title_score > 60 or subtitle_score > 60:
+ filtered.append(item)
+
+ assert len(filtered) >= 1
+ assert any(item["title"] == "Python" for item in filtered)
+
+ def test_fuzzy_match_partial(self, sample_items):
+ """Test fuzzy partial matching."""
+ query = "Jav"
+ filtered = []
+
+ for item in sample_items:
+ title = str(item.get("title", ""))
+ subtitle = str(item.get("subtitle", ""))
+ title_score = fuzz.partial_ratio(query, title)
+ subtitle_score = fuzz.partial_ratio(query, subtitle)
+ if title_score > 60 or subtitle_score > 60:
+ filtered.append(item)
+
+ assert len(filtered) >= 1
+ assert any(item["title"] == "JavaScript" for item in filtered)
+
+ def test_fuzzy_match_transposition(self, sample_items):
+ """Test fuzzy match with character transposition."""
+ query = "Rsut" # Characters swapped
+ filtered = []
+
+ for item in sample_items:
+ title = str(item.get("title", ""))
+ subtitle = str(item.get("subtitle", ""))
+ title_score = fuzz.partial_ratio(query, title)
+ subtitle_score = fuzz.partial_ratio(query, subtitle)
+ if title_score > 60 or subtitle_score > 60:
+ filtered.append(item)
+
+ assert len(filtered) >= 1
+ assert any(item["title"] == "Rust" for item in filtered)
+
+ def test_fuzzy_no_match(self, sample_items):
+ """Test fuzzy search with no close matches."""
+ query = "xyz123"
+ filtered = []
+
+ for item in sample_items:
+ title = str(item.get("title", ""))
+ subtitle = str(item.get("subtitle", ""))
+ title_score = fuzz.partial_ratio(query, title)
+ subtitle_score = fuzz.partial_ratio(query, subtitle)
+ if title_score > 60 or subtitle_score > 60:
+ filtered.append(item)
+
+ assert len(filtered) == 0
+
+
+class TestFilteringWithCustomKeys:
+ """Test filtering with custom key configurations."""
+
+ def test_filter_custom_title_key(self, custom_key_items):
+ """Test filtering with custom title key."""
+ query = "alice"
+ filtered = []
+ title_key = "name"
+ subtitle_key = "role"
+
+ for item in custom_key_items:
+ title = str(item.get(title_key, "")).lower()
+ subtitle = str(item.get(subtitle_key, "")).lower()
+ if query in title or query in subtitle:
+ filtered.append(item)
+
+ assert len(filtered) == 1
+ assert filtered[0]["name"] == "Alice"
+
+ def test_filter_custom_subtitle_key(self, custom_key_items):
+ """Test filtering with custom subtitle key."""
+ query = "engineer"
+ filtered = []
+ title_key = "name"
+ subtitle_key = "role"
+
+ for item in custom_key_items:
+ title = str(item.get(title_key, "")).lower()
+ subtitle = str(item.get(subtitle_key, "")).lower()
+ if query in title or query in subtitle:
+ filtered.append(item)
+
+ assert len(filtered) == 1
+ assert filtered[0]["role"] == "Engineer"
+
+ def test_filter_no_subtitle_key(self, single_line_items):
+ """Test filtering when subtitle_key is None."""
+ query = "config"
+ filtered = []
+ title_key = "filename"
+
+ for item in single_line_items:
+ title = str(item.get(title_key, "")).lower()
+ if query in title:
+ filtered.append(item)
+
+ assert len(filtered) == 1
+ assert filtered[0]["filename"] == "config.json"
+
+
+class TestEmptyQueryFiltering:
+ """Test filtering with empty query."""
+
+ def test_empty_query_returns_all(self, sample_items):
+ """Test that empty query returns all items."""
+ query = ""
+ filtered = sample_items.copy() if not query else []
+
+ assert len(filtered) == len(sample_items)
+
+ def test_whitespace_query(self, sample_items):
+ """Test filtering with whitespace-only query."""
+ query = " "
+ filtered = []
+
+ # Whitespace query should not match anything
+ for item in sample_items:
+ title = str(item.get("title", "")).lower()
+ subtitle = str(item.get("subtitle", "")).lower()
+ if query.strip() and (query.strip() in title or query.strip() in subtitle):
+ filtered.append(item)
+
+ assert len(filtered) == 0
+
+
+class TestFilteringEdgeCases:
+ """Test edge cases in filtering."""
+
+ def test_filter_with_missing_fields(self):
+ """Test filtering when items are missing title/subtitle."""
+ items = [
+ {"other_field": "value"},
+ {"title": "Has Title"},
+ ]
+
+ query = "title"
+ filtered = []
+
+ for item in items:
+ title = str(item.get("title", "")).lower()
+ subtitle = str(item.get("subtitle", "")).lower()
+ if query in title or query in subtitle:
+ filtered.append(item)
+
+ assert len(filtered) == 1
+
+ def test_filter_with_numeric_values(self):
+ """Test filtering with numeric values in fields."""
+ items = [
+ {"title": "Item 123", "subtitle": "Test"},
+ {"title": "Item 456", "subtitle": "Test"},
+ ]
+
+ query = "123"
+ filtered = []
+
+ for item in items:
+ title = str(item.get("title", "")).lower()
+ subtitle = str(item.get("subtitle", "")).lower()
+ if query in title or query in subtitle:
+ filtered.append(item)
+
+ assert len(filtered) == 1
+
+ def test_filter_special_characters(self):
+ """Test filtering with special characters."""
+ items = [
+ {"title": "test@example.com", "subtitle": "Email"},
+ {"title": "user-name", "subtitle": "Username"},
+ ]
+
+ query = "@"
+ filtered = []
+
+ for item in items:
+ title = str(item.get("title", "")).lower()
+ subtitle = str(item.get("subtitle", "")).lower()
+ if query in title or query in subtitle:
+ filtered.append(item)
+
+ assert len(filtered) == 1
+ assert "@" in filtered[0]["title"]
diff --git a/tests/test_integration.py b/tests/test_integration.py
new file mode 100644
index 0000000..bf6beca
--- /dev/null
+++ b/tests/test_integration.py
@@ -0,0 +1,286 @@
+from selectui import SelectItem, SelectUI
+
+
+class TestPydanticIntegration:
+ """Integration tests for Pydantic SelectItem with SelectUI."""
+
+ def test_selectui_with_selectitems(self):
+ """Test SelectUI works with SelectItem instances."""
+ items = [
+ SelectItem(title="Python", subtitle="Language", info="1991"),
+ SelectItem(title="Rust", subtitle="Systems", info="2010"),
+ ]
+
+ app = SelectUI(items=items, oneshot=True)
+
+ assert len(app.items) == 2
+ assert app.items[0]["title"] == "Python"
+ assert app.items[1]["title"] == "Rust"
+
+ def test_selectui_preserves_selectitem_data(self):
+ """Test that SelectUI preserves SelectItem data field."""
+ items = [
+ SelectItem(
+ title="Python",
+ subtitle="Language",
+ data={"website": "python.org", "typing": "dynamic"}
+ ),
+ ]
+
+ app = SelectUI(items=items)
+
+ assert app.items[0]["website"] == "python.org"
+ assert app.items[0]["typing"] == "dynamic"
+
+ def test_mixed_items_not_allowed(self):
+ """Test that items list should be homogeneous."""
+ # Note: Current implementation assumes all items are same type
+ # This test documents the behavior
+ items = [SelectItem(title="A")]
+ app = SelectUI(items=items)
+
+ # Should convert to dicts
+ assert isinstance(app.items[0], dict)
+
+
+class TestDictToPydanticConversion:
+ """Test converting dict items to Pydantic and back."""
+
+ def test_from_dict_to_selectui(self, custom_key_items):
+ """Test converting custom dict items to SelectItem then to SelectUI."""
+ items = [
+ SelectItem.from_dict(
+ item,
+ title_key="name",
+ subtitle_key="role",
+ info_key="team"
+ )
+ for item in custom_key_items
+ ]
+
+ app = SelectUI(items=items, oneshot=True)
+
+ assert len(app.items) == 3
+ assert app.items[0]["title"] == "Alice"
+
+ def test_json_to_selectitem_to_selectui(self):
+ """Test full pipeline: JSON -> SelectItem -> SelectUI."""
+ json_data = [
+ {"product": "Laptop", "price": "$1299", "stock": 45},
+ {"product": "Mouse", "price": "$29", "stock": 200},
+ ]
+
+ items = [
+ SelectItem.from_dict(
+ item,
+ title_key="product",
+ subtitle_key=None,
+ info_key="price"
+ )
+ for item in json_data
+ ]
+
+ app = SelectUI(items=items)
+
+ assert len(app.items) == 2
+ assert app.items[0]["title"] == "Laptop"
+ assert app.items[0]["info"] == "$1299"
+ assert app.items[0]["stock"] == 45
+
+
+class TestCustomKeyMappings:
+ """Integration tests for custom key mappings."""
+
+ def test_selectui_with_custom_keys_dict(self, custom_key_items):
+ """Test SelectUI with dict items and custom keys."""
+ app = SelectUI(
+ items=custom_key_items,
+ title_key="name",
+ subtitle_key="role",
+ info_key="team"
+ )
+
+ assert app.title_key == "name"
+ assert app.subtitle_key == "role"
+ assert app.info_key == "team"
+
+ def test_selectui_with_custom_keys_pydantic(self):
+ """Test SelectUI with SelectItem and custom keys."""
+ items = [
+ SelectItem(title="Alice", subtitle="Engineer", info="Backend"),
+ ]
+
+ app = SelectUI(
+ items=items,
+ title_key="name",
+ subtitle_key="role",
+ info_key="team"
+ )
+
+ # Should have both standard and custom keys
+ assert app.items[0]["title"] == "Alice"
+ assert app.items[0]["name"] == "Alice"
+
+
+class TestRealWorldScenarios:
+ """Test real-world usage scenarios."""
+
+ def test_github_api_response_format(self):
+ """Test handling GitHub-style API response."""
+ repos = [
+ {
+ "full_name": "python/cpython",
+ "description": "The Python programming language",
+ "stargazers_count": 50000,
+ "html_url": "https://github.com/python/cpython"
+ },
+ {
+ "full_name": "rust-lang/rust",
+ "description": "Empowering everyone to build reliable software",
+ "stargazers_count": 80000,
+ "html_url": "https://github.com/rust-lang/rust"
+ },
+ ]
+
+ items = [
+ SelectItem.from_dict(
+ repo,
+ title_key="full_name",
+ subtitle_key="description",
+ info_key="stargazers_count"
+ )
+ for repo in repos
+ ]
+
+ app = SelectUI(items=items, oneshot=True)
+
+ assert len(app.items) == 2
+ assert app.items[0]["html_url"] == "https://github.com/python/cpython"
+
+ def test_database_query_results(self):
+ """Test handling database query results."""
+ users = [
+ {"id": 1, "username": "alice", "email": "alice@example.com", "role": "admin"},
+ {"id": 2, "username": "bob", "email": "bob@example.com", "role": "user"},
+ ]
+
+ items = [
+ SelectItem.from_dict(
+ user,
+ title_key="username",
+ subtitle_key="email",
+ info_key="role"
+ )
+ for user in users
+ ]
+
+ app = SelectUI(items=items)
+
+ assert len(app.items) == 2
+ assert app.items[0]["id"] == 1
+ assert app.items[0]["title"] == "alice"
+
+ def test_file_listing_scenario(self):
+ """Test file listing scenario."""
+ files = [
+ {"filename": "config.json", "size": 1024, "modified": "2024-01-15"},
+ {"filename": "data.csv", "size": 4096, "modified": "2024-01-14"},
+ ]
+
+ items = [
+ SelectItem.from_dict(
+ file,
+ title_key="filename",
+ subtitle_key=None,
+ info_key="size"
+ )
+ for file in files
+ ]
+
+ app = SelectUI(items=items, oneshot=True)
+
+ assert len(app.items) == 2
+ assert app.items[0]["modified"] == "2024-01-15"
+
+
+class TestEdgeCasesIntegration:
+ """Integration tests for edge cases."""
+
+ def test_empty_items_to_selectui(self):
+ """Test SelectUI with empty items list."""
+ items = []
+ app = SelectUI(items=items)
+
+ assert app.items == []
+ assert app.filtered_items == []
+
+ def test_single_item(self):
+ """Test SelectUI with single item."""
+ items = [SelectItem(title="Only Item")]
+ app = SelectUI(items=items)
+
+ assert len(app.items) == 1
+
+ def test_large_number_of_items(self):
+ """Test SelectUI with many items."""
+ items = [
+ SelectItem(title=f"Item {i}", subtitle=f"Subtitle {i}")
+ for i in range(1000)
+ ]
+
+ app = SelectUI(items=items)
+
+ assert len(app.items) == 1000
+
+ def test_items_with_unicode(self):
+ """Test SelectUI with Unicode characters."""
+ items = [
+ SelectItem(title="Python 🐍", subtitle="Programming"),
+ SelectItem(title="Rust 🦀", subtitle="Systems"),
+ SelectItem(title="日本語", subtitle="Japanese"),
+ ]
+
+ app = SelectUI(items=items)
+
+ assert len(app.items) == 3
+ assert app.items[0]["title"] == "Python 🐍"
+ assert app.items[2]["title"] == "日本語"
+
+ def test_items_with_special_chars(self):
+ """Test SelectUI with special characters."""
+ items = [
+ SelectItem(title="test@example.com", subtitle="Email"),
+ SelectItem(title="user-name_123", subtitle="Username"),
+ SelectItem(title="$100.00", subtitle="Price"),
+ ]
+
+ app = SelectUI(items=items)
+
+ assert len(app.items) == 3
+ assert "@" in app.items[0]["title"]
+ assert "$" in app.items[2]["title"]
+
+
+class TestBackwardCompatibility:
+ """Test backward compatibility with plain dicts."""
+
+ def test_plain_dicts_still_work(self, sample_items):
+ """Test that plain dict items still work."""
+ app = SelectUI(items=sample_items, oneshot=True)
+
+ assert len(app.items) == 4
+ assert isinstance(app.items[0], dict)
+
+ def test_mixed_usage_patterns(self):
+ """Test that both dict and Pydantic patterns work."""
+ # Pattern 1: Plain dicts
+ app1 = SelectUI(items=[{"title": "A"}])
+ assert app1.items[0]["title"] == "A"
+
+ # Pattern 2: Pydantic models
+ app2 = SelectUI(items=[SelectItem(title="B")])
+ assert app2.items[0]["title"] == "B"
+
+ # Both should work identically
+ assert isinstance(app1.items[0], dict)
+ assert isinstance(app2.items[0], dict)
diff --git a/tests/test_models.py b/tests/test_models.py
new file mode 100644
index 0000000..0fa2735
--- /dev/null
+++ b/tests/test_models.py
@@ -0,0 +1,240 @@
+import pytest
+from pydantic import ValidationError
+
+from selectui import SelectItem
+
+
+class TestSelectItemCreation:
+ """Test SelectItem model creation."""
+
+ def test_create_with_all_fields(self):
+ """Test creating SelectItem with all fields."""
+ item = SelectItem(
+ title="Python",
+ subtitle="Programming language",
+ info="1991",
+ data={"website": "python.org", "typing": "dynamic"}
+ )
+
+ assert item.title == "Python"
+ assert item.subtitle == "Programming language"
+ assert item.info == "1991"
+ assert item.data["website"] == "python.org"
+ assert item.data["typing"] == "dynamic"
+
+ def test_create_with_required_only(self):
+ """Test creating SelectItem with only required fields."""
+ item = SelectItem(title="Minimal")
+
+ assert item.title == "Minimal"
+ assert item.subtitle is None
+ assert item.info is None
+ assert item.data == {}
+
+ def test_create_with_optional_fields(self):
+ """Test creating SelectItem with some optional fields."""
+ item = SelectItem(title="Python", subtitle="Language")
+
+ assert item.title == "Python"
+ assert item.subtitle == "Language"
+ assert item.info is None
+ assert item.data == {}
+
+
+class TestSelectItemValidation:
+ """Test SelectItem validation."""
+
+ def test_empty_title_fails(self):
+ """Test that empty title raises validation error."""
+ with pytest.raises(ValidationError) as exc_info:
+ SelectItem(title="")
+
+ error_msg = str(exc_info.value).lower()
+ assert "string should have at least 1 character" in error_msg or "title cannot be empty" in error_msg
+
+ def test_whitespace_title_fails(self):
+ """Test that whitespace-only title raises validation error."""
+ with pytest.raises(ValidationError) as exc_info:
+ SelectItem(title=" ")
+
+ assert "title cannot be empty" in str(exc_info.value).lower()
+
+ def test_title_with_spaces_succeeds(self):
+ """Test that title with spaces (but not only spaces) is valid."""
+ item = SelectItem(title="Hello World")
+ assert item.title == "Hello World"
+
+ def test_missing_title_fails(self):
+ """Test that missing title raises validation error."""
+ with pytest.raises((ValidationError, TypeError)):
+ SelectItem() # type: ignore
+
+
+class TestSelectItemToDict:
+ """Test SelectItem.to_dict() method."""
+
+ def test_to_dict_all_fields(self):
+ """Test converting SelectItem with all fields to dict."""
+ item = SelectItem(
+ title="Python",
+ subtitle="Language",
+ info="1991",
+ data={"extra": "field"}
+ )
+
+ result = item.to_dict()
+
+ assert result["title"] == "Python"
+ assert result["subtitle"] == "Language"
+ assert result["info"] == "1991"
+ assert result["extra"] == "field"
+
+ def test_to_dict_minimal(self):
+ """Test converting minimal SelectItem to dict."""
+ item = SelectItem(title="Test")
+ result = item.to_dict()
+
+ assert result["title"] == "Test"
+ assert "subtitle" not in result
+ assert "info" not in result
+
+ def test_to_dict_with_custom_data(self):
+ """Test that custom data is preserved in to_dict."""
+ item = SelectItem(
+ title="Item",
+ data={"custom1": "value1", "custom2": "value2"}
+ )
+
+ result = item.to_dict()
+
+ assert result["custom1"] == "value1"
+ assert result["custom2"] == "value2"
+
+
+class TestSelectItemFromDict:
+ """Test SelectItem.from_dict() method."""
+
+ def test_from_dict_standard_keys(self):
+ """Test from_dict with standard keys."""
+ data = {
+ "title": "Python",
+ "subtitle": "Language",
+ "info": "1991",
+ "extra": "data"
+ }
+
+ item = SelectItem.from_dict(data)
+
+ assert item.title == "Python"
+ assert item.subtitle == "Language"
+ assert item.info == "1991"
+ assert item.data["extra"] == "data"
+
+ def test_from_dict_custom_keys(self):
+ """Test from_dict with custom key mappings."""
+ data = {
+ "name": "Alice",
+ "role": "Engineer",
+ "team": "Backend",
+ "level": "Senior"
+ }
+
+ item = SelectItem.from_dict(
+ data,
+ title_key="name",
+ subtitle_key="role",
+ info_key="team"
+ )
+
+ assert item.title == "Alice"
+ assert item.subtitle == "Engineer"
+ assert item.info == "Backend"
+ assert item.data["level"] == "Senior"
+
+ def test_from_dict_missing_optional_keys(self):
+ """Test from_dict when optional keys are missing."""
+ data = {"title": "Only Title"}
+
+ item = SelectItem.from_dict(data)
+
+ assert item.title == "Only Title"
+ assert item.subtitle is None
+ assert item.info is None
+
+ def test_from_dict_no_subtitle(self):
+ """Test from_dict with subtitle_key=None."""
+ data = {"filename": "test.py", "size": 1024}
+
+ item = SelectItem.from_dict(
+ data,
+ title_key="filename",
+ subtitle_key=None,
+ info_key="size"
+ )
+
+ assert item.title == "test.py"
+ assert item.subtitle is None
+ assert item.info == "1024"
+
+ def test_from_dict_preserves_all_data(self):
+ """Test that from_dict preserves all original data."""
+ data = {
+ "name": "Alice",
+ "role": "Engineer",
+ "extra1": "value1",
+ "extra2": "value2"
+ }
+
+ item = SelectItem.from_dict(data, title_key="name", subtitle_key="role")
+
+ assert item.data["name"] == "Alice"
+ assert item.data["role"] == "Engineer"
+ assert item.data["extra1"] == "value1"
+ assert item.data["extra2"] == "value2"
+
+ def test_from_dict_type_conversion(self):
+ """Test that from_dict converts values to strings."""
+ data = {
+ "title": 123,
+ "subtitle": True,
+ "info": 456.78
+ }
+
+ item = SelectItem.from_dict(data)
+
+ assert item.title == "123"
+ assert item.subtitle == "True"
+ assert item.info == "456.78"
+
+
+class TestSelectItemRoundTrip:
+ """Test SelectItem conversion round trips."""
+
+ def test_roundtrip_standard_keys(self):
+ """Test SelectItem -> dict -> SelectItem with standard keys."""
+ original = SelectItem(
+ title="Test",
+ subtitle="Subtitle",
+ info="Info",
+ data={"custom": "field"}
+ )
+
+ # Convert to dict and back
+ data = original.to_dict()
+ restored = SelectItem.from_dict(data)
+
+ assert restored.title == original.title
+ assert restored.subtitle == original.subtitle
+ assert restored.info == original.info
+ assert restored.data["custom"] == original.data["custom"]
+
+ def test_roundtrip_minimal(self):
+ """Test round trip with minimal SelectItem."""
+ original = SelectItem(title="Minimal")
+
+ data = original.to_dict()
+ restored = SelectItem.from_dict(data)
+
+ assert restored.title == original.title
+ assert restored.subtitle == original.subtitle
+ assert restored.info == original.info
diff --git a/tests/test_selectui.py b/tests/test_selectui.py
new file mode 100644
index 0000000..991a065
--- /dev/null
+++ b/tests/test_selectui.py
@@ -0,0 +1,272 @@
+from selectui import SelectItem, SelectUI
+
+
+class TestSelectUIInitialization:
+ """Test SelectUI initialization."""
+
+ def test_init_with_dict_items(self, sample_items):
+ """Test initialization with dictionary items."""
+ app = SelectUI(items=sample_items)
+
+ assert len(app.items) == 4
+ assert app.items[0]["title"] == "Python"
+ assert app.oneshot is False
+ assert app.fuzzy_search is False
+ assert app.case_sensitive is False
+
+ def test_init_with_pydantic_items(self):
+ """Test initialization with SelectItem instances."""
+ items = [
+ SelectItem(title="Python", subtitle="Language"),
+ SelectItem(title="Rust", subtitle="Systems"),
+ ]
+
+ app = SelectUI(items=items)
+
+ assert len(app.items) == 2
+ assert app.items[0]["title"] == "Python"
+
+ def test_init_empty(self):
+ """Test initialization with no items."""
+ app = SelectUI()
+
+ assert app.items == []
+ assert app.filtered_items == []
+
+ def test_init_with_oneshot(self, sample_items):
+ """Test initialization with oneshot mode."""
+ app = SelectUI(items=sample_items, oneshot=True)
+
+ assert app.oneshot is True
+
+ def test_init_with_custom_keys(self, sample_items):
+ """Test initialization with custom key configuration."""
+ app = SelectUI(
+ items=sample_items,
+ title_key="name",
+ subtitle_key="desc",
+ info_key="year"
+ )
+
+ assert app.title_key == "name"
+ assert app.subtitle_key == "desc"
+ assert app.info_key == "year"
+
+ def test_init_with_events_mode(self, sample_items):
+ """Test initialization with events mode."""
+ app = SelectUI(items=sample_items, events_mode=True)
+
+ assert app.events_mode is True
+
+ def test_init_with_command_template(self):
+ """Test initialization with command template."""
+ app = SelectUI(command_template="echo {}")
+
+ assert app.command_template == "echo {}"
+ assert app.input_mode is True
+
+
+class TestSelectUIFiltering:
+ """Test SelectUI filtering functionality."""
+
+ def test_filter_items_exact_match(self, sample_items):
+ """Test exact string filtering."""
+ app = SelectUI(items=sample_items)
+ app.fuzzy_search = False
+ app.case_sensitive = False
+
+ # Simulate filtering for "python"
+ filtered = []
+ query = "python"
+ for item in app.items:
+ title = str(item.get("title", "")).lower()
+ subtitle = str(item.get("subtitle", "")).lower()
+ if query in title or query in subtitle:
+ filtered.append(item)
+
+ assert len(filtered) == 1
+ assert filtered[0]["title"] == "Python"
+
+ def test_filter_items_case_sensitive(self, sample_items):
+ """Test case-sensitive filtering."""
+ app = SelectUI(items=sample_items)
+
+ # Lowercase query should not match "Python" in case-sensitive mode
+ filtered = []
+ query = "python"
+ for item in app.items:
+ title = str(item.get("title", ""))
+ subtitle = str(item.get("subtitle", ""))
+ if query in title or query in subtitle:
+ filtered.append(item)
+
+ assert len(filtered) == 0
+
+ # Correct case should match
+ filtered = []
+ query = "Python"
+ for item in app.items:
+ title = str(item.get("title", ""))
+ subtitle = str(item.get("subtitle", ""))
+ if query in title or query in subtitle:
+ filtered.append(item)
+
+ assert len(filtered) == 1
+
+ def test_filter_by_subtitle(self, sample_items):
+ """Test filtering by subtitle."""
+ app = SelectUI(items=sample_items)
+
+ filtered = []
+ query = "language"
+ for item in app.items:
+ title = str(item.get("title", "")).lower()
+ subtitle = str(item.get("subtitle", "")).lower()
+ if query in title or query in subtitle:
+ filtered.append(item)
+
+ assert len(filtered) == 4 # All items have "language" in subtitle
+
+
+class TestSelectUINormalization:
+ """Test item normalization in SelectUI."""
+
+ def test_normalize_dict_items(self, sample_items):
+ """Test that dict items are normalized correctly."""
+ app = SelectUI(items=sample_items)
+
+ assert isinstance(app.items, list)
+ assert all(isinstance(item, dict) for item in app.items)
+
+ def test_normalize_pydantic_items(self):
+ """Test that Pydantic items are converted to dicts."""
+ items = [
+ SelectItem(title="Python", subtitle="Language", info="1991"),
+ SelectItem(title="Rust", subtitle="Systems", info="2010"),
+ ]
+
+ app = SelectUI(items=items)
+
+ assert isinstance(app.items, list)
+ assert all(isinstance(item, dict) for item in app.items)
+ assert app.items[0]["title"] == "Python"
+ assert app.items[1]["title"] == "Rust"
+
+ def test_normalize_with_custom_keys(self):
+ """Test normalization preserves custom key mappings."""
+ items = [
+ SelectItem(title="Alice", subtitle="Engineer", info="Backend"),
+ ]
+
+ app = SelectUI(
+ items=items,
+ title_key="name",
+ subtitle_key="role",
+ info_key="team"
+ )
+
+ # Should have both standard and custom keys
+ assert app.items[0]["title"] == "Alice"
+ assert app.items[0]["name"] == "Alice"
+ assert app.items[0]["subtitle"] == "Engineer"
+ assert app.items[0]["role"] == "Engineer"
+
+
+class TestSelectUIState:
+ """Test SelectUI state management."""
+
+ def test_initial_state(self, sample_items):
+ """Test initial state after initialization."""
+ app = SelectUI(items=sample_items)
+
+ assert app.selected_item is None
+ assert app.fuzzy_search is False
+ assert app.case_sensitive is False
+ assert app._stream_complete is False
+ assert app._refresh_timer_running is False
+
+ def test_filtered_items_initial(self, sample_items):
+ """Test that filtered_items initially equals items."""
+ app = SelectUI(items=sample_items)
+
+ assert app.filtered_items == app.items
+
+ def test_command_mode_state(self):
+ """Test state in command mode."""
+ app = SelectUI(command_template="echo {}")
+
+ assert app.input_mode is True
+ assert app.command_template == "echo {}"
+ assert app._command_running is False
+
+ def test_stdin_mode_state(self):
+ """Test state when stdin_fd is not provided."""
+ app = SelectUI()
+
+ assert app.stdin_fd is None
+ assert app._stream_complete is False
+
+
+class TestSelectUIEdgeCases:
+ """Test edge cases and error handling."""
+
+ def test_empty_items_list(self):
+ """Test with empty items list."""
+ app = SelectUI(items=[])
+
+ assert app.items == []
+ assert app.filtered_items == []
+
+ def test_none_items(self):
+ """Test with None items."""
+ app = SelectUI(items=None)
+
+ assert app.items == []
+ assert app.filtered_items == []
+
+ def test_items_without_required_keys(self):
+ """Test items missing title key."""
+ items = [{"name": "Test"}] # Missing 'title' key
+
+ app = SelectUI(items=items, title_key="name")
+
+ assert len(app.items) == 1
+
+ def test_mixed_item_types(self):
+ """Test that dict items work after Pydantic items."""
+ pydantic_items = [SelectItem(title="A")]
+ dict_items = [{"title": "B"}]
+
+ app1 = SelectUI(items=pydantic_items)
+ app2 = SelectUI(items=dict_items)
+
+ assert app1.items[0]["title"] == "A"
+ assert app2.items[0]["title"] == "B"
+
+
+class TestSelectUICustomKeys:
+ """Test custom key configuration."""
+
+ def test_custom_title_key(self, custom_key_items):
+ """Test using custom title key."""
+ app = SelectUI(items=custom_key_items, title_key="name")
+
+ assert app.title_key == "name"
+
+ def test_custom_subtitle_key(self, custom_key_items):
+ """Test using custom subtitle key."""
+ app = SelectUI(items=custom_key_items, subtitle_key="role")
+
+ assert app.subtitle_key == "role"
+
+ def test_custom_info_key(self, custom_key_items):
+ """Test using custom info key."""
+ app = SelectUI(items=custom_key_items, info_key="team")
+
+ assert app.info_key == "team"
+
+ def test_no_subtitle_key(self, single_line_items):
+ """Test with subtitle_key=None for single-line display."""
+ app = SelectUI(items=single_line_items, subtitle_key=None)
+
+ assert app.subtitle_key is None