Files
2026-01-15 15:27:48 +01:00

393 lines
11 KiB
GDScript

extends GdUnitTestSuite
var runner: GdUnitSceneRunner
var world: World
func before():
runner = scene_runner("res://addons/gecs/tests/test_scene.tscn")
world = runner.get_property("world")
ECS.world = world
func after_test():
world.purge(false)
func test_observer_receive_component_changed():
world.add_system(TestASystem.new())
var test_a_observer = TestAObserver.new()
world.add_observer(test_a_observer)
# Create entities with the required components
var entity_a = TestA.new()
entity_a.name = "a"
entity_a.add_component(C_TestA.new())
var entity_b = TestB.new()
entity_b.name = "b"
entity_b.add_component(C_TestA.new())
entity_b.add_component(C_TestB.new())
# issue #43
var entity_a2 = TestA.new()
entity_a2.name = "a"
entity_a2.add_component(C_TestA.new())
world.get_node(world.entity_nodes_root).add_child(entity_a2)
world.add_entity(entity_a2, null, false)
assert_int(test_a_observer.added_count).is_equal(1)
# Add some entities before systems
world.add_entities([entity_a, entity_b])
assert_int(test_a_observer.added_count).is_equal(3)
# Run the systems once
print('process 1st')
world.process(0.1)
# Check the event_count
assert_int(test_a_observer.event_count).is_equal(2)
# Run the systems again
print('process 2nd')
world.process(0.1)
# Check the event_count
assert_int(test_a_observer.event_count).is_equal(4)
## Test that observers detect when a component is added to an entity
func test_observer_on_component_added():
var observer = O_ObserverTest.new()
world.add_observer(observer)
# Create an entity without the component
var entity = Entity.new()
world.add_entity(entity)
# Verify observer hasn't fired yet
assert_int(observer.added_count).is_equal(0)
# Add the watched component
var component = C_ObserverTest.new()
entity.add_component(component)
# Verify observer detected the addition
assert_int(observer.added_count).is_equal(1)
assert_object(observer.last_added_entity).is_equal(entity)
## Test that observers detect when a component is removed from an entity
func test_observer_on_component_removed():
var observer = O_ObserverTest.new()
world.add_observer(observer)
# Create an entity with the component
var entity = Entity.new()
var component = C_ObserverTest.new()
entity.add_component(component)
world.add_entity(entity)
# Verify observer detected the addition
assert_int(observer.added_count).is_equal(1)
# Reset and remove the component
observer.reset()
entity.remove_component(C_ObserverTest)
# Verify observer detected the removal
assert_int(observer.removed_count).is_equal(1)
assert_object(observer.last_removed_entity).is_equal(entity)
assert_int(observer.added_count).is_equal(0) # Should remain 0 after reset
## Test that observers detect property changes on watched components
func test_observer_on_component_changed():
var observer = O_ObserverTest.new()
world.add_observer(observer)
# Create an entity with the component
var entity = Entity.new()
var component = C_ObserverTest.new(0, "initial")
entity.add_component(component)
world.add_entity(entity)
# Reset the observer (it may have fired on add)
observer.reset()
# Change the value property (this will emit property_changed signal)
component.value = 42
# Verify observer detected the change
assert_int(observer.changed_count).is_equal(1)
assert_object(observer.last_changed_entity).is_equal(entity)
assert_str(observer.last_changed_property).is_equal("value")
assert_int(observer.last_old_value).is_equal(0)
assert_int(observer.last_new_value).is_equal(42)
# Change another property
component.name_prop = "changed"
# Verify observer detected the second change
assert_int(observer.changed_count).is_equal(2)
assert_str(observer.last_changed_property).is_equal("name_prop")
assert_str(observer.last_old_value).is_equal("initial")
assert_str(observer.last_new_value).is_equal("changed")
## Test that observers respect query filters (only match entities that pass the query)
func test_observer_respects_query_filter():
var health_observer = O_HealthObserver.new()
world.add_observer(health_observer)
# Create entity with only health component (should NOT match - needs both components)
var entity_only_health = Entity.new()
entity_only_health.add_component(C_ObserverHealth.new())
world.add_entity(entity_only_health)
# Observer should NOT have fired (doesn't match query)
assert_int(health_observer.health_added_count).is_equal(0)
# Create entity with both components (should match)
var entity_both = Entity.new()
entity_both.add_component(C_ObserverTest.new())
entity_both.add_component(C_ObserverHealth.new())
world.add_entity(entity_both)
# Observer should have fired now (matches query)
assert_int(health_observer.health_added_count).is_equal(1)
## Test that multiple observers can watch the same component
func test_multiple_observers_same_component():
var observer1 = O_ObserverTest.new()
var observer2 = O_ObserverTest.new()
world.add_observer(observer1)
world.add_observer(observer2)
# Create an entity with the component
var entity = Entity.new()
var component = C_ObserverTest.new()
entity.add_component(component)
world.add_entity(entity)
# Both observers should have detected the addition
assert_int(observer1.added_count).is_equal(1)
assert_int(observer2.added_count).is_equal(1)
# Change the component
observer1.reset()
observer2.reset()
component.value = 100
# Both observers should have detected the change
assert_int(observer1.changed_count).is_equal(1)
assert_int(observer2.changed_count).is_equal(1)
## Test that observers can track multiple property changes
func test_observer_tracks_multiple_changes():
var observer = O_ObserverTest.new()
world.add_observer(observer)
# Create an entity with the component
var entity = Entity.new()
var component = C_ObserverTest.new(0, "start")
entity.add_component(component)
world.add_entity(entity)
observer.reset()
# Make multiple changes
component.value = 10
component.value = 20
component.name_prop = "middle"
component.value = 30
# Should have detected all 4 changes
assert_int(observer.changed_count).is_equal(4)
## Test observer with health component and query matching
func test_observer_health_low_health_alert():
var health_observer = O_HealthObserver.new()
world.add_observer(health_observer)
# Create entity with both components
var entity = Entity.new()
entity.add_component(C_ObserverTest.new())
var health = C_ObserverHealth.new(100)
entity.add_component(health)
world.add_entity(entity)
health_observer.reset()
# Reduce health gradually
health.health = 50
assert_int(health_observer.health_changed_count).is_equal(1)
assert_int(health_observer.low_health_alerts.size()).is_equal(0)
health.health = 25 # Below threshold
assert_int(health_observer.health_changed_count).is_equal(2)
assert_int(health_observer.low_health_alerts.size()).is_equal(1)
assert_object(health_observer.low_health_alerts[0]).is_equal(entity)
## Test that observer doesn't fire when entity doesn't match query
func test_observer_ignores_non_matching_entities():
var health_observer = O_HealthObserver.new()
world.add_observer(health_observer)
# Create entity with only C_ObserverTest (not both components)
var entity = Entity.new()
entity.add_component(C_ObserverTest.new())
world.add_entity(entity)
# Try to add C_ObserverHealth to a different entity that doesn't have C_ObserverTest
var entity2 = Entity.new()
entity2.add_component(C_ObserverHealth.new())
world.add_entity(entity2)
# Observer should not have fired (entity2 doesn't match query)
assert_int(health_observer.health_added_count).is_equal(0)
## Test observer detects component addition before entity is added to world
func test_observer_component_added_before_entity_added():
var observer = O_ObserverTest.new()
world.add_observer(observer)
# Create entity and add component BEFORE adding to world
var entity = Entity.new()
var component = C_ObserverTest.new()
entity.add_component(component)
# Observer shouldn't have fired yet
assert_int(observer.added_count).is_equal(0)
# Now add to world
world.add_entity(entity)
# Observer should fire now
assert_int(observer.added_count).is_equal(1)
## Test observer with component replacement
func test_observer_component_replacement():
var observer = O_ObserverTest.new()
world.add_observer(observer)
# Create entity with component
var entity = Entity.new()
var component1 = C_ObserverTest.new(10, "first")
entity.add_component(component1)
world.add_entity(entity)
assert_int(observer.added_count).is_equal(1)
# Replace the component (add_component on same type replaces)
var component2 = C_ObserverTest.new(20, "second")
entity.add_component(component2)
# Should trigger both removed and added
assert_int(observer.removed_count).is_equal(1)
assert_int(observer.added_count).is_equal(2)
## Test that property changes without signal emission don't trigger observer
func test_observer_ignores_direct_property_changes():
var observer = O_ObserverTest.new()
world.add_observer(observer)
# Create entity with component
var entity = Entity.new()
var component = C_ObserverTest.new()
entity.add_component(component)
world.add_entity(entity)
observer.reset()
# Directly set the property WITHOUT using the setter
# This bypasses the property_changed signal
# Note: In GDScript, using the property name always calls the setter,
# so we need to access the internal variable directly
# For this test, we're verifying that ONLY setters that emit signals work
# Using the setter (should trigger)
component.value = 42
assert_int(observer.changed_count).is_equal(1)
# The framework correctly requires explicit signal emission in setters
## Test observer with entity that starts matching query after component addition
func test_observer_entity_becomes_matching():
var health_observer = O_HealthObserver.new()
world.add_observer(health_observer)
# Create entity with only one component
var entity = Entity.new()
entity.add_component(C_ObserverTest.new())
world.add_entity(entity)
# Health observer shouldn't fire (needs both components)
assert_int(health_observer.health_added_count).is_equal(0)
# Add the second component
entity.add_component(C_ObserverHealth.new())
# Now health observer should fire
assert_int(health_observer.health_added_count).is_equal(1)
## Test removing observer from world
func test_remove_observer():
var observer = O_ObserverTest.new()
world.add_observer(observer)
# Create entity with component
var entity = Entity.new()
entity.add_component(C_ObserverTest.new())
world.add_entity(entity)
assert_int(observer.added_count).is_equal(1)
# Remove the observer
world.remove_observer(observer)
# Add another entity - observer should not fire
var entity2 = Entity.new()
entity2.add_component(C_ObserverTest.new())
world.add_entity(entity2)
# Count should still be 1 (not 2)
assert_int(observer.added_count).is_equal(1)
## Test observer with multiple entities
func test_observer_with_multiple_entities():
var observer = O_ObserverTest.new()
world.add_observer(observer)
# Create multiple entities
for i in range(5):
var entity = Entity.new()
entity.add_component(C_ObserverTest.new(i))
world.add_entity(entity)
# Should have detected all 5 additions
assert_int(observer.added_count).is_equal(5)
observer.reset()
# Get all entities and modify their components
var entities = world.query.with_all([C_ObserverTest]).execute()
for entity in entities:
var comp = entity.get_component(C_ObserverTest)
comp.value = comp.value + 100
# Should have detected all 5 changes
assert_int(observer.changed_count).is_equal(5)