"""
Comprehensive tests for _build_component universality and robustness.
Tests various scenarios:
1. Basic widget types (DecoratedText, TextParagraph, Image, etc.)
2. Container components (ButtonList, ChipList, Grid, Columns)
3. Empty components (Divider)
4. Nested structures with children
5. DAG validation (valid/invalid relationships)
6. Edge cases (missing params, unknown components)
7. build_component_tree for hierarchies
"""
import json
import logging
import sys
from pathlib import Path
from typing import Any, Dict, List, Optional
# Setup path
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
logging.basicConfig(level=logging.INFO, format="%(message)s")
logger = logging.getLogger(__name__)
def test_build_component():
"""Run comprehensive _build_component tests."""
from gchat.card_builder import SmartCardBuilderV2
builder = SmartCardBuilderV2()
wrapper = builder._get_wrapper()
results = {
"passed": 0,
"failed": 0,
"tests": [],
}
def run_test(name: str, test_fn):
"""Run a single test and track results."""
try:
result = test_fn()
if result.get("success"):
results["passed"] += 1
status = "✅ PASS"
else:
results["failed"] += 1
status = "❌ FAIL"
results["tests"].append({"name": name, "status": status, **result})
logger.info(f"{status}: {name}")
if result.get("output"):
logger.info(
f" Output: {json.dumps(result['output'], indent=2)[:200]}..."
)
if result.get("error"):
logger.info(f" Error: {result['error']}")
except Exception as e:
results["failed"] += 1
results["tests"].append(
{"name": name, "status": "❌ EXCEPTION", "error": str(e)}
)
logger.info(f"❌ EXCEPTION: {name} - {e}")
# =========================================================================
# 1. BASIC WIDGET TYPES
# =========================================================================
logger.info("\n" + "=" * 60)
logger.info("1. BASIC WIDGET TYPES")
logger.info("=" * 60)
def test_decorated_text():
result = builder._build_component(
"DecoratedText",
{"text": "Hello World", "wrapText": True},
wrap_with_key=True,
)
success = (
result is not None
and "decoratedText" in result
and result["decoratedText"].get("text") == "Hello World"
)
return {"success": success, "output": result}
def test_decorated_text_with_labels():
result = builder._build_component(
"DecoratedText",
{"text": "Status", "top_label": "System", "bottom_label": "Active"},
wrap_with_key=True,
)
success = result is not None and "decoratedText" in result
return {"success": success, "output": result}
def test_text_paragraph():
result = builder._build_component(
"TextParagraph",
{"text": "This is a paragraph of text."},
wrap_with_key=True,
)
success = result is not None and "textParagraph" in result
return {"success": success, "output": result}
def test_image():
result = builder._build_component(
"Image",
{"imageUrl": "https://example.com/image.png"},
wrap_with_key=True,
)
success = result is not None and "image" in result
return {"success": success, "output": result}
def test_button():
result = builder._build_component(
"Button",
{"text": "Click Me", "url": "https://example.com"},
wrap_with_key=True,
)
success = result is not None and "button" in result
return {"success": success, "output": result}
run_test("DecoratedText basic", test_decorated_text)
run_test("DecoratedText with labels", test_decorated_text_with_labels)
run_test("TextParagraph", test_text_paragraph)
run_test("Image", test_image)
run_test("Button", test_button)
# =========================================================================
# 2. CONTAINER COMPONENTS
# =========================================================================
logger.info("\n" + "=" * 60)
logger.info("2. CONTAINER COMPONENTS")
logger.info("=" * 60)
def test_button_list_empty():
result = builder._build_component(
"ButtonList",
{},
wrap_with_key=True,
)
success = result is not None and "buttonList" in result
return {"success": success, "output": result}
def test_button_list_with_children():
# Pre-built button children
button1 = {
"button": {
"text": "Button 1",
"onClick": {"openLink": {"url": "https://a.com"}},
}
}
button2 = {
"button": {
"text": "Button 2",
"onClick": {"openLink": {"url": "https://b.com"}},
}
}
result = builder._build_component(
"ButtonList",
{},
wrap_with_key=True,
children=[button1, button2],
)
success = (
result is not None
and "buttonList" in result
and "buttons" in result.get("buttonList", {})
)
return {"success": success, "output": result}
def test_chip_list():
result = builder._build_component(
"ChipList",
{},
wrap_with_key=True,
)
success = result is not None and "chipList" in result
return {"success": success, "output": result}
def test_grid():
result = builder._build_component(
"Grid",
{"columnCount": 2},
wrap_with_key=True,
)
success = result is not None and "grid" in result
return {"success": success, "output": result}
def test_columns():
result = builder._build_component(
"Columns",
{},
wrap_with_key=True,
)
success = result is not None and "columns" in result
return {"success": success, "output": result}
run_test("ButtonList empty", test_button_list_empty)
run_test("ButtonList with children", test_button_list_with_children)
run_test("ChipList", test_chip_list)
run_test("Grid", test_grid)
run_test("Columns", test_columns)
# =========================================================================
# 3. EMPTY COMPONENTS
# =========================================================================
logger.info("\n" + "=" * 60)
logger.info("3. EMPTY COMPONENTS")
logger.info("=" * 60)
def test_divider():
result = builder._build_component(
"Divider",
{},
wrap_with_key=True,
)
success = result is not None and "divider" in result
return {"success": success, "output": result}
run_test("Divider (empty component)", test_divider)
# =========================================================================
# 4. NESTED STRUCTURES
# =========================================================================
logger.info("\n" + "=" * 60)
logger.info("4. NESTED STRUCTURES")
logger.info("=" * 60)
def test_section_with_widgets():
# Build widgets first
text_widget = builder._build_component(
"DecoratedText", {"text": "Hello"}, wrap_with_key=True
)
button_list = builder._build_component(
"ButtonList",
{},
wrap_with_key=True,
children=[{"button": {"text": "Click"}}],
)
# Build section with widgets
result = builder._build_component(
"Section",
{"header": "My Section"},
wrap_with_key=True,
children=[text_widget, button_list],
)
success = (
result is not None
and "section" in result
and "widgets" in result.get("section", {})
)
return {"success": success, "output": result}
def test_decorated_text_with_button():
"""Test DecoratedText with inline button (nested child)."""
button = {
"button": {
"text": "Action",
"onClick": {"openLink": {"url": "https://example.com"}},
}
}
result = builder._build_component(
"DecoratedText",
{"text": "Click the button →"},
wrap_with_key=True,
children=[button],
)
success = result is not None and "decoratedText" in result
return {"success": success, "output": result}
run_test("Section with widgets", test_section_with_widgets)
run_test("DecoratedText with button", test_decorated_text_with_button)
# =========================================================================
# 5. DAG VALIDATION
# =========================================================================
logger.info("\n" + "=" * 60)
logger.info("5. DAG VALIDATION")
logger.info("=" * 60)
def test_valid_parent_child():
"""Test valid Section -> DecoratedText relationship."""
text_widget = builder._build_component(
"DecoratedText", {"text": "Valid child"}, wrap_with_key=True
)
result = builder._build_component(
"Section",
{},
wrap_with_key=True,
children=[text_widget],
validate=True,
)
success = result is not None and "section" in result
return {"success": success, "output": result}
def test_can_contain_check():
"""Test wrapper.can_contain() directly."""
if wrapper and hasattr(wrapper, "can_contain"):
# Section can contain DecoratedText
valid = wrapper.can_contain("Section", "DecoratedText")
# Section cannot directly contain Icon (needs ButtonList->Button->Icon)
indirect = wrapper.can_contain("Section", "Icon")
direct_only = wrapper.can_contain("Section", "Icon", direct_only=True)
return {
"success": valid,
"output": {
"Section->DecoratedText": valid,
"Section->Icon (any depth)": indirect,
"Section->Icon (direct only)": direct_only,
},
}
return {"success": False, "error": "can_contain not available"}
run_test("Valid parent-child with validate=True", test_valid_parent_child)
run_test("can_contain() DAG queries", test_can_contain_check)
# =========================================================================
# 6. EDGE CASES
# =========================================================================
logger.info("\n" + "=" * 60)
logger.info("6. EDGE CASES")
logger.info("=" * 60)
def test_unknown_component():
"""Test handling of unknown component type."""
result = builder._build_component(
"NonExistentWidget",
{"foo": "bar"},
wrap_with_key=True,
)
# Should return fallback dict, not crash
success = result is not None
return {"success": success, "output": result}
def test_empty_params():
"""Test component with no params."""
result = builder._build_component(
"DecoratedText",
{},
wrap_with_key=True,
)
success = result is not None
return {"success": success, "output": result}
def test_wrap_with_key_false():
"""Test wrap_with_key=False returns inner dict."""
result = builder._build_component(
"DecoratedText",
{"text": "Inner only"},
wrap_with_key=False,
)
# Should NOT have decoratedText wrapper key
success = result is not None and "decoratedText" not in result
return {"success": success, "output": result}
def test_no_wrapper():
"""Test fallback when wrapper is None."""
result = builder._build_component(
"DecoratedText",
{"text": "No wrapper"},
wrapper=None, # Force no wrapper path
wrap_with_key=True,
)
# _get_wrapper() will still be called, so this tests the internal path
success = result is not None
return {"success": success, "output": result}
run_test("Unknown component (graceful fallback)", test_unknown_component)
run_test("Empty params", test_empty_params)
run_test("wrap_with_key=False", test_wrap_with_key_false)
run_test("Fallback path", test_no_wrapper)
# =========================================================================
# 7. BUILD_COMPONENT_TREE
# =========================================================================
logger.info("\n" + "=" * 60)
logger.info("7. BUILD_COMPONENT_TREE (Hierarchies)")
logger.info("=" * 60)
def test_simple_tree():
"""Test simple component tree."""
tree = {
"component": "DecoratedText",
"params": {"text": "Simple tree test", "wrapText": True},
}
result = builder.build_component_tree(tree)
success = result is not None and "decoratedText" in result
return {"success": success, "output": result}
def test_nested_tree():
"""Test nested component tree with Section -> widgets."""
tree = {
"component": "Section",
"params": {"header": "Test Section"},
"children": [
{
"component": "DecoratedText",
"params": {"text": "Item 1", "wrapText": True},
},
{
"component": "DecoratedText",
"params": {"text": "Item 2", "wrapText": True},
},
{
"component": "ButtonList",
"children": [
{
"component": "Button",
"params": {"text": "Action", "url": "https://test.com"},
},
],
},
],
}
result = builder.build_component_tree(tree, validate=True)
success = (
result is not None
and "section" in result
and "widgets" in result.get("section", {})
and len(result["section"]["widgets"]) >= 2
)
return {"success": success, "output": result}
def test_deeply_nested_tree():
"""Test deeply nested tree (Section -> Columns -> Column -> widgets)."""
tree = {
"component": "Section",
"children": [
{
"component": "Columns",
"children": [
{
"component": "Column",
"children": [
{
"component": "DecoratedText",
"params": {"text": "Col 1"},
},
],
},
{
"component": "Column",
"children": [
{
"component": "DecoratedText",
"params": {"text": "Col 2"},
},
],
},
],
},
],
}
result = builder.build_component_tree(tree, validate=True)
success = result is not None and "section" in result
return {"success": success, "output": result}
def test_tree_missing_component():
"""Test tree with missing component key."""
tree = {"params": {"text": "No component key"}}
result = builder.build_component_tree(tree)
success = result is None # Should return None for invalid tree
return {"success": success, "output": result}
run_test("Simple tree (single component)", test_simple_tree)
run_test("Nested tree (Section with children)", test_nested_tree)
run_test("Deeply nested tree (Section -> Columns)", test_deeply_nested_tree)
run_test("Tree with missing component key", test_tree_missing_component)
# =========================================================================
# 8. FORM COMPONENTS
# =========================================================================
logger.info("\n" + "=" * 60)
logger.info("8. FORM COMPONENTS")
logger.info("=" * 60)
def test_text_input():
result = builder._build_component(
"TextInput",
{"name": "user_name", "label": "Your Name"},
wrap_with_key=True,
)
success = result is not None and "textInput" in result
return {"success": success, "output": result}
def test_selection_input():
result = builder._build_component(
"SelectionInput",
{"name": "choice", "label": "Pick one", "type": "DROPDOWN"},
wrap_with_key=True,
)
success = result is not None and "selectionInput" in result
return {"success": success, "output": result}
def test_date_time_picker():
result = builder._build_component(
"DateTimePicker",
{"name": "date", "label": "Select date"},
wrap_with_key=True,
)
success = result is not None and "dateTimePicker" in result
return {"success": success, "output": result}
run_test("TextInput", test_text_input)
run_test("SelectionInput", test_selection_input)
run_test("DateTimePicker", test_date_time_picker)
# =========================================================================
# 9. CAROUSEL COMPONENTS
# =========================================================================
logger.info("\n" + "=" * 60)
logger.info("9. CAROUSEL COMPONENTS")
logger.info("=" * 60)
def test_carousel():
result = builder._build_component(
"Carousel",
{},
wrap_with_key=True,
)
success = result is not None
return {"success": success, "output": result}
def test_carousel_card():
result = builder._build_component(
"CarouselCard",
{"title": "Card 1", "subtitle": "Description"},
wrap_with_key=True,
)
success = result is not None
return {"success": success, "output": result}
run_test("Carousel", test_carousel)
run_test("CarouselCard", test_carousel_card)
# =========================================================================
# SUMMARY
# =========================================================================
logger.info("\n" + "=" * 60)
logger.info("SUMMARY")
logger.info("=" * 60)
logger.info(f"Total: {results['passed'] + results['failed']} tests")
logger.info(f"Passed: {results['passed']}")
logger.info(f"Failed: {results['failed']}")
if results["failed"] > 0:
logger.info("\nFailed tests:")
for test in results["tests"]:
if "FAIL" in test["status"] or "EXCEPTION" in test["status"]:
logger.info(f" - {test['name']}: {test.get('error', 'See output')}")
return results
if __name__ == "__main__":
test_build_component()