From b863698f6e63870c54a72cbce278b447089686cc Mon Sep 17 00:00:00 2001 From: John McCardle Date: Mon, 29 Dec 2025 19:48:00 -0500 Subject: [PATCH] test: Add comprehensive Scene object API test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Demonstrates the object-oriented Scene API as alternative to module-level functions. Key features tested: - Scene object creation and properties (name, active, children) - scene.activate() vs mcrfpy.setScene() - scene.on_key property - can be set on ANY scene, not just current - Scene visual properties (pos, visible, opacity) - Subclassing for lifecycle callbacks (on_enter, on_exit, update) The on_key advantage resolves confusion with keypressScene() which only works on the currently active scene. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- tests/unit/test_scene_object_api.py | 235 ++++++++++++++++++++++++++++ 1 file changed, 235 insertions(+) create mode 100644 tests/unit/test_scene_object_api.py diff --git a/tests/unit/test_scene_object_api.py b/tests/unit/test_scene_object_api.py new file mode 100644 index 0000000..f52c1d4 --- /dev/null +++ b/tests/unit/test_scene_object_api.py @@ -0,0 +1,235 @@ +#!/usr/bin/env python3 +"""Test the object-oriented Scene API (alternative to module-level functions). + +The Scene object provides an OOP approach to scene management with several advantages: +1. `scene.on_key` can be set on ANY scene, not just the current one +2. `scene.children` provides direct access to UI elements +3. Subclassing enables lifecycle callbacks (on_enter, on_exit, update, etc.) + +This is the recommended approach for new code, replacing: +- mcrfpy.createScene(name) -> scene = mcrfpy.Scene(name) +- mcrfpy.setScene(name) -> scene.activate() +- mcrfpy.sceneUI(name) -> scene.children +- mcrfpy.keypressScene(callback) -> scene.on_key = callback +""" +import mcrfpy +import sys + +def test_scene_object_basics(): + """Test basic Scene object creation and properties.""" + print("=== Test: Scene Object Basics ===") + + # Create scene using object-oriented approach + scene = mcrfpy.Scene("oop_test") + + # Check name property + assert scene.name == "oop_test", f"Expected 'oop_test', got '{scene.name}'" + print(f" name: {scene.name}") + + # Check active property (should be False, not yet activated) + print(f" active: {scene.active}") + + # Check children property returns UICollection + children = scene.children + print(f" children type: {type(children).__name__}") + print(f" children count: {len(children)}") + + # Add UI elements via children + frame = mcrfpy.Frame(pos=(50, 50), size=(200, 100), fill_color=mcrfpy.Color(100, 100, 200)) + scene.children.append(frame) + print(f" children count after append: {len(scene.children)}") + + print(" PASS: Basic properties work correctly") + return scene + +def test_scene_activation(): + """Test scene activation.""" + print("\n=== Test: Scene Activation ===") + + scene1 = mcrfpy.Scene("scene_a") + scene2 = mcrfpy.Scene("scene_b") + + # Neither active yet + print(f" Before activation - scene1.active: {scene1.active}, scene2.active: {scene2.active}") + + # Activate scene1 + scene1.activate() + print(f" After scene1.activate() - scene1.active: {scene1.active}, scene2.active: {scene2.active}") + assert scene1.active == True, "scene1 should be active" + assert scene2.active == False, "scene2 should not be active" + + # Activate scene2 + scene2.activate() + print(f" After scene2.activate() - scene1.active: {scene1.active}, scene2.active: {scene2.active}") + assert scene1.active == False, "scene1 should not be active now" + assert scene2.active == True, "scene2 should be active" + + print(" PASS: Scene activation works correctly") + +def test_scene_on_key(): + """Test setting on_key callback on scene objects. + + This is the KEY ADVANTAGE over module-level keypressScene(): + You can set on_key on ANY scene, not just the current one! + """ + print("\n=== Test: Scene on_key Property ===") + + scene1 = mcrfpy.Scene("keys_scene1") + scene2 = mcrfpy.Scene("keys_scene2") + + # Track which callback was called + callback_log = [] + + def scene1_keyhandler(key, action): + callback_log.append(("scene1", key, action)) + + def scene2_keyhandler(key, action): + callback_log.append(("scene2", key, action)) + + # Set callbacks on BOTH scenes BEFORE activating either + # This is impossible with keypressScene() which only works on current scene! + scene1.on_key = scene1_keyhandler + scene2.on_key = scene2_keyhandler + + print(f" scene1.on_key set: {scene1.on_key is not None}") + print(f" scene2.on_key set: {scene2.on_key is not None}") + + # Verify callbacks are retrievable + assert callable(scene1.on_key), "scene1.on_key should be callable" + assert callable(scene2.on_key), "scene2.on_key should be callable" + + # Test clearing callback + scene1.on_key = None + assert scene1.on_key is None, "scene1.on_key should be None after clearing" + print(" scene1.on_key cleared successfully") + + # Re-set it + scene1.on_key = scene1_keyhandler + + print(" PASS: on_key property works correctly") + +def test_scene_visual_properties(): + """Test scene-level visual properties (pos, visible, opacity).""" + print("\n=== Test: Scene Visual Properties ===") + + scene = mcrfpy.Scene("visual_props_test") + + # Test pos property + print(f" Initial pos: {scene.pos}") + scene.pos = (100, 50) + print(f" After setting pos=(100, 50): {scene.pos}") + + # Test visible property + print(f" Initial visible: {scene.visible}") + scene.visible = False + print(f" After setting visible=False: {scene.visible}") + assert scene.visible == False, "visible should be False" + scene.visible = True + + # Test opacity property + print(f" Initial opacity: {scene.opacity}") + scene.opacity = 0.5 + print(f" After setting opacity=0.5: {scene.opacity}") + assert 0.49 < scene.opacity < 0.51, f"opacity should be ~0.5, got {scene.opacity}" + + print(" PASS: Visual properties work correctly") + +def test_scene_subclass(): + """Test subclassing Scene for lifecycle callbacks.""" + print("\n=== Test: Scene Subclass with Lifecycle ===") + + class GameScene(mcrfpy.Scene): + def __init__(self, name): + super().__init__(name) + self.enter_count = 0 + self.exit_count = 0 + self.update_count = 0 + + def on_enter(self): + self.enter_count += 1 + print(f" GameScene.on_enter() called (count: {self.enter_count})") + + def on_exit(self): + self.exit_count += 1 + print(f" GameScene.on_exit() called (count: {self.exit_count})") + + def on_keypress(self, key, action): + print(f" GameScene.on_keypress({key}, {action})") + + def update(self, dt): + self.update_count += 1 + # Note: update is called every frame, so we don't print + + game_scene = GameScene("game_scene_test") + other_scene = mcrfpy.Scene("other_scene_test") + + # Add some UI to game scene + game_scene.children.append( + mcrfpy.Caption(pos=(100, 100), text="Game Scene", fill_color=mcrfpy.Color(255, 255, 255)) + ) + + print(f" Created GameScene with {len(game_scene.children)} children") + print(f" enter_count before activation: {game_scene.enter_count}") + + # Activate - should trigger on_enter + game_scene.activate() + print(f" enter_count after activation: {game_scene.enter_count}") + + # Switch away - should trigger on_exit + other_scene.activate() + print(f" exit_count after switching: {game_scene.exit_count}") + + print(" PASS: Subclassing works correctly") + +def test_comparison_with_module_functions(): + """Demonstrate the difference between old and new approaches.""" + print("\n=== Comparison: Module Functions vs Scene Objects ===") + + print("\n OLD APPROACH (module-level functions):") + print(" mcrfpy.createScene('my_scene')") + print(" mcrfpy.setScene('my_scene')") + print(" ui = mcrfpy.sceneUI('my_scene')") + print(" ui.append(mcrfpy.Frame(...))") + print(" mcrfpy.keypressScene(handler) # ONLY works on current scene!") + + print("\n NEW APPROACH (Scene objects):") + print(" scene = mcrfpy.Scene('my_scene')") + print(" scene.activate()") + print(" scene.children.append(mcrfpy.Frame(...))") + print(" scene.on_key = handler # Works on ANY scene!") + + print("\n KEY BENEFITS:") + print(" 1. scene.on_key can be set on non-active scenes") + print(" 2. Subclassing enables on_enter/on_exit/update callbacks") + print(" 3. Object reference makes code more readable") + print(" 4. scene.children is clearer than sceneUI(name)") + + print("\n PASS: Documentation complete") + +def main(): + """Run all Scene object API tests.""" + print("=" * 60) + print("Scene Object API Test Suite") + print("=" * 60) + + try: + test_scene_object_basics() + test_scene_activation() + test_scene_on_key() + test_scene_visual_properties() + test_scene_subclass() + test_comparison_with_module_functions() + + print("\n" + "=" * 60) + print("ALL TESTS PASSED!") + print("=" * 60) + sys.exit(0) + + except Exception as e: + print(f"\nTEST FAILED: {e}") + import traceback + traceback.print_exc() + sys.exit(1) + +if __name__ == "__main__": + main()