"""Tests for Viser viewer update-policy helpers.""" from dataclasses import dataclass from typing import Any from unittest.mock import MagicMock import mujoco from mjviser import ViserMujocoScene from mjlab.viewer.viser.overlays import ViserContactOverlays, ViserDebugOverlays from mjlab.viewer.viser.viewer import ViserPlayViewer @dataclass class _DummyScene: env_idx: int debug_visualization_enabled: bool show_contact_points: bool = False show_contact_forces: bool = False needs_update: bool = False clear_count: int = 0 clear_debug_count: int = 0 def clear(self) -> None: self.clear_count += 1 def clear_debug_all(self) -> None: self.clear_debug_count += 1 class _DummyEnv: def __init__(self, unwrapped: Any): self._unwrapped = unwrapped @property def unwrapped(self) -> Any: return self._unwrapped def test_should_update_cameras(): assert ViserPlayViewer._should_update_cameras(paused=False, has_pending_updates=False) assert not ViserPlayViewer._should_update_cameras( paused=True, has_pending_updates=False ) assert ViserPlayViewer._should_update_cameras(paused=True, has_pending_updates=True) def test_should_submit_scene_update(): # Odd ticks are skipped to keep scene submits around 30Hz. assert not ViserPlayViewer._should_submit_scene_update( counter=1, paused=False, has_pending_updates=True ) # Running: submit on even ticks regardless of pending flags. assert ViserPlayViewer._should_submit_scene_update( counter=2, paused=False, has_pending_updates=False ) # Paused: submit only with pending updates. assert not ViserPlayViewer._should_submit_scene_update( counter=2, paused=True, has_pending_updates=False ) assert ViserPlayViewer._should_submit_scene_update( counter=2, paused=True, has_pending_updates=True ) def test_scene_requires_live_refresh(): """Base scene sets needs_update when contact overlays are visible.""" model = mujoco.MjModel.from_xml_string("") server = MagicMock() scene = ViserMujocoScene(server, model, num_envs=1) scene.show_contact_points = False scene.show_contact_forces = False scene.needs_update = False scene._last_body_xpos = None # No cached data, refresh is a no-op. scene.refresh_visualization() assert not scene.needs_update scene.show_contact_points = True assert scene.show_contact_points or scene.show_contact_forces def test_debug_overlays_env_switch_and_queue(): """env_switch respects enabled flag; queue clears + calls update_visualizers.""" unwrapped = MagicMock(spec=["update_visualizers"]) env = _DummyEnv(unwrapped) scene = _DummyScene(env_idx=0, debug_visualization_enabled=False) overlays = ViserDebugOverlays(env=env, scene=scene) # Disabled: env switch does not clear. overlays.on_env_switch() assert scene.clear_debug_count == 0 # Enabled: env switch clears, queue dispatches. scene.debug_visualization_enabled = True overlays.on_env_switch() assert scene.clear_debug_count == 1 overlays.queue() assert scene.clear_count == 1 unwrapped.update_visualizers.assert_called_once_with(scene) def test_contact_overlays_env_switch(): """env_switch requests scene update only when contacts are enabled.""" scene = _DummyScene(env_idx=0, debug_visualization_enabled=False) overlays = ViserContactOverlays(scene=scene) assert not overlays.is_enabled() overlays.on_env_switch() assert not scene.needs_update scene.show_contact_points = True assert overlays.is_enabled() overlays.on_env_switch() assert scene.needs_update