diff options
| author | Louis Burda <dev@sinitax.com> | 2026-02-28 18:54:19 +0100 |
|---|---|---|
| committer | Louis Burda <dev@sinitax.com> | 2026-02-28 18:54:19 +0100 |
| commit | be1dd21f8e4fbd5361531b4d8727a0d0d243e8ec (patch) | |
| tree | e7b540012e0510d1304d2dac8e137545ae103f75 /tests | |
| parent | d70a199a72bf9a69eb4a3fcf166b0435918b2e33 (diff) | |
| download | selectui-main.tar.gz selectui-main.zip | |
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/conftest.py | 34 | ||||
| -rw-r--r-- | tests/test_basic.py | 13 | ||||
| -rw-r--r-- | tests/test_filtering.py | 318 | ||||
| -rw-r--r-- | tests/test_integration.py | 286 | ||||
| -rw-r--r-- | tests/test_models.py | 240 | ||||
| -rw-r--r-- | tests/test_selectui.py | 272 |
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 |
