update
This commit is contained in:
@@ -38,11 +38,25 @@ def e2e_device_dump_injector():
|
||||
|
||||
return _inject_dump
|
||||
|
||||
class VirtualClock:
|
||||
def __init__(self):
|
||||
self.time = 0.0
|
||||
self.animation_target_time = 0.0
|
||||
|
||||
def sleep(self, seconds):
|
||||
if hasattr(seconds, '__iter__'):
|
||||
return # For edge case where something weird is passed
|
||||
self.time += float(seconds)
|
||||
|
||||
clock = VirtualClock()
|
||||
|
||||
@pytest.fixture
|
||||
def dynamic_e2e_dump_injector(monkeypatch):
|
||||
"""
|
||||
State-Machine Injector: Replaces dump_hierarchy dynamically when transitions occur.
|
||||
Validates that the Telepathic Engine's pathfinding truly worked.
|
||||
It now inherently simulates UI animation delays. If a dump is requested
|
||||
LESS than 1.5 virtual seconds after a transition, it returns a garbage animating UI.
|
||||
"""
|
||||
def _inject(device_mock, state_map, initial_xml):
|
||||
fix_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "fixtures")
|
||||
@@ -54,22 +68,54 @@ def dynamic_e2e_dump_injector(monkeypatch):
|
||||
with open(path, "r") as f:
|
||||
return f.read()
|
||||
|
||||
device_mock.deviceV2.dump_hierarchy.return_value = load_xml(initial_xml)
|
||||
# The current active state XML
|
||||
device_mock._current_active_xml = load_xml(initial_xml)
|
||||
|
||||
def _dump_hierarchy_hook():
|
||||
# If the clock hasn't advanced past the UI animation time, return garbage
|
||||
# Actually, explicitly fail the E2E test because the bot missed a sync guard!
|
||||
if clock.time < clock.animation_target_time:
|
||||
pytest.fail(f"UI SYNCHRONIZATION FAILURE: dump_hierarchy() called mid-animation! "
|
||||
f"Virtual Clock is at {clock.time:.1f}s but UI needs until {clock.animation_target_time:.1f}s to settle. "
|
||||
f"Add a time.sleep() guard before interacting with the UI after a click.", pytrace=False)
|
||||
return device_mock._current_active_xml
|
||||
|
||||
device_mock.deviceV2.dump_hierarchy.side_effect = _dump_hierarchy_hook
|
||||
|
||||
from GramAddict.core.telepathic_engine import TelepathicEngine
|
||||
|
||||
def _mock_find_best_node(*args, **kwargs):
|
||||
return {"x": 500, "y": 500, "skip": False, "score": 1.0, "source": "e2e_mock"}
|
||||
|
||||
monkeypatch.setattr(TelepathicEngine, "find_best_node", _mock_find_best_node)
|
||||
monkeypatch.setattr(TelepathicEngine, "verify_success", lambda *args, **kwargs: True)
|
||||
|
||||
from GramAddict.core.q_nav_graph import QNavGraph
|
||||
original_execute = QNavGraph._execute_transition
|
||||
|
||||
def _mock_execute_transition(nav_self, action, zero_engine):
|
||||
def _mock_execute_transition(nav_self, action, zero_engine=None, max_retries=2):
|
||||
if action == 'tap_post_username':
|
||||
return True
|
||||
|
||||
# Evaluate using the real internal LLM/Keyword logic against the current mock XML!
|
||||
success = original_execute(nav_self, action, zero_engine)
|
||||
if success is True and action in state_map:
|
||||
# The node was clicked successfully! Swap the XML to the target state.
|
||||
device_mock.deviceV2.dump_hierarchy.return_value = load_xml(state_map[action])
|
||||
return success
|
||||
# We need to trigger the UI change exactly when the robot clicks physically
|
||||
original_click = nav_self.device.click
|
||||
|
||||
def _click_hook(obj=None, *args, **kwargs):
|
||||
original_click(obj, *args, **kwargs)
|
||||
if action in state_map:
|
||||
device_mock._current_active_xml = load_xml(state_map[action])
|
||||
clock.animation_target_time = clock.time + 1.5
|
||||
|
||||
nav_self.device.click = _click_hook
|
||||
|
||||
try:
|
||||
# Evaluate using the real internal LLM/Keyword logic against the current mock XML!
|
||||
# Note: max_retries parameter needs to be passed through
|
||||
success = original_execute(nav_self, action, zero_engine, max_retries=max_retries)
|
||||
return success
|
||||
finally:
|
||||
nav_self.device.click = original_click
|
||||
|
||||
monkeypatch.setattr(QNavGraph, "_execute_transition", _mock_execute_transition)
|
||||
|
||||
return _inject
|
||||
@@ -77,12 +123,34 @@ def dynamic_e2e_dump_injector(monkeypatch):
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_all_delays(monkeypatch):
|
||||
"""
|
||||
Strips out all humanized hardware delays specifically for the E2E test suite.
|
||||
Ensures loops evaluate instantly using the injected dumps.
|
||||
Replaces all humanized hardware delays specifically for the E2E test suite
|
||||
with a Virtual Clock. Ensures loops evaluate instantly but preserves chronological
|
||||
dependency for our Animation Simulator.
|
||||
"""
|
||||
monkeypatch.setattr(time, "sleep", lambda x: None)
|
||||
monkeypatch.setattr(utils, "random_sleep", lambda *args, **kwargs: None)
|
||||
monkeypatch.setattr(utils, "sleep", lambda x: None)
|
||||
global clock
|
||||
clock.time = 0.0 # reset for test
|
||||
clock.animation_target_time = 0.0
|
||||
|
||||
def simulate_sleep(seconds):
|
||||
clock.sleep(seconds)
|
||||
|
||||
money_sleep = lambda x: simulate_sleep(x)
|
||||
random_sleep = lambda *args, **kwargs: simulate_sleep(1.0) # Assume 1.0 minimum for randoms
|
||||
|
||||
monkeypatch.setattr(time, "sleep", money_sleep)
|
||||
monkeypatch.setattr(utils, "random_sleep", random_sleep)
|
||||
monkeypatch.setattr(utils, "sleep", money_sleep)
|
||||
|
||||
# Needs to capture specific module sleeps depending on how they imported it
|
||||
try:
|
||||
from GramAddict.core import bot_flow
|
||||
monkeypatch.setattr(bot_flow, "sleep", money_sleep)
|
||||
monkeypatch.setattr(bot_flow.random, "uniform", lambda a, b: float(a)) # deterministic lower bound
|
||||
|
||||
from GramAddict.core import q_nav_graph
|
||||
monkeypatch.setattr(q_nav_graph.random, "uniform", lambda a, b: float(a))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Standardize DarwinEngine across tests to prevent mockup math errors on session end
|
||||
try:
|
||||
|
||||
26
tests/e2e/test_e2e_animation_timing.py
Normal file
26
tests/e2e/test_e2e_animation_timing.py
Normal file
@@ -0,0 +1,26 @@
|
||||
import pytest
|
||||
import time
|
||||
from unittest.mock import MagicMock
|
||||
from GramAddict.core.q_nav_graph import QNavGraph
|
||||
|
||||
def test_animation_sync_guard_catches_missing_sleep(dynamic_e2e_dump_injector):
|
||||
"""
|
||||
Proves that the new Animation Simulator built into conftest.py
|
||||
properly throws an error if we query the UI without waiting for animations.
|
||||
"""
|
||||
device = MagicMock()
|
||||
# Inject dummy states
|
||||
dynamic_e2e_dump_injector(device, {'tap_explore_tab': 'explore_feed_dump.xml'}, "home_feed_with_ad.xml")
|
||||
|
||||
# Simulate a raw bug where the developer clicked but didn't sleep
|
||||
# We will simulate exactly what _execute_transition tries to do
|
||||
|
||||
nav = QNavGraph(device)
|
||||
|
||||
# We call transition. QNavGraph internally clicks and sleeps for 1.2s minimum.
|
||||
# Our Animation target is 1.5s, so the dump inside _execute_transition will hit the fail guard!
|
||||
from _pytest.outcomes import Failed
|
||||
with pytest.raises(Failed) as exc_info:
|
||||
nav._execute_transition("tap_explore_tab")
|
||||
|
||||
assert "UI SYNCHRONIZATION FAILURE" in str(exc_info.value), "The simulator failed to catch the missing sleep guard!"
|
||||
Reference in New Issue
Block a user