1818import pytest
1919
2020from roborock .cli import _await_q10_map_push , cli
21- from roborock .data .b01_q10 .b01_q10_code_mappings import B01_Q10_DP
21+ from roborock .data .b01_q10 .b01_q10_code_mappings import B01_Q10_DP , YXDeviceState
2222from roborock .devices .traits .b01 .q10 import Q10PropertiesApi , create
2323from roborock .devices .traits .b01 .q10 .map import MapContentTrait , MapDpsTrait
2424from roborock .exceptions import RoborockException
3737TRACE_SESSION_FIXTURE = Path ("tests/map/testdata/b01_q10_trace_session.bin" )
3838
3939
40- def _map_trait () -> MapContentTrait :
40+ def _map_trait (map_dps : MapDpsTrait | None = None ) -> MapContentTrait :
4141 """Create a high-level trait with its required low-level dependency."""
42- return MapContentTrait (MapDpsTrait ())
42+ return MapContentTrait (map_dps or MapDpsTrait ())
4343
4444
4545def _zone_blob () -> str :
@@ -238,7 +238,7 @@ def test_render_failure_clears_stale_image() -> None:
238238def test_map_dps_update_renders_decoded_overlays () -> None :
239239 """A DPS update recomposes an existing map with decoded overlays."""
240240 map_dps = MapDpsTrait ()
241- trait = MapContentTrait (map_dps )
241+ trait = _map_trait (map_dps )
242242 packet = parse_map_packet (FIXTURE .read_bytes ())
243243 notified : list [None ] = []
244244 trait .add_update_listener (lambda : notified .append (None ))
@@ -263,7 +263,7 @@ def test_map_dps_update_renders_decoded_overlays() -> None:
263263def test_map_dps_blobs_are_decoded_only_when_dps_arrives () -> None :
264264 """Map and trace renders reuse the overlays decoded by the DPS trait."""
265265 map_dps = MapDpsTrait ()
266- trait = MapContentTrait (map_dps )
266+ trait = _map_trait (map_dps )
267267
268268 with (
269269 patch ("roborock.devices.traits.b01.q10.map.parse_zone_blob" , return_value = []) as parse_zones ,
@@ -291,7 +291,7 @@ def test_load_overlays_partial_update_keeps_existing_zones() -> None:
291291def test_map_dps_update_without_map_does_not_notify_map_content () -> None :
292292 """A DPS update cannot change high-level content before a map arrives."""
293293 map_dps = MapDpsTrait ()
294- trait = MapContentTrait (map_dps )
294+ trait = _map_trait (map_dps )
295295 notified = []
296296 trait .add_update_listener (lambda : notified .append (True ))
297297
@@ -304,11 +304,110 @@ def test_map_dps_update_without_map_does_not_notify_map_content() -> None:
304304def test_map_dps_push_without_overlay_data_points_is_noop () -> None :
305305 """A DPS push carrying neither overlay DP leaves both traits untouched."""
306306 map_dps = MapDpsTrait ()
307- trait = MapContentTrait (map_dps )
307+ trait = _map_trait (map_dps )
308308 notified = []
309309 trait .add_update_listener (lambda : notified .append (True ))
310310
311311 map_dps .update_from_dps ({B01_Q10_DP .BATTERY : 50 })
312312
313313 assert map_dps .overlays == Q10MapOverlays ()
314314 assert not notified
315+
316+
317+ # --- Dock state --------------------------------------------------------------
318+
319+
320+ def test_charging_status_renders_robot_at_dock () -> None :
321+ """Charging status adds the idle robot marker without inventing a path."""
322+ map_dps = MapDpsTrait ()
323+ trait = _map_trait (map_dps )
324+ packet = parse_map_packet (FIXTURE .read_bytes ())
325+ notified : list [None ] = []
326+ trait .add_update_listener (lambda : notified .append (None ))
327+
328+ with patch (
329+ "roborock.devices.traits.b01.q10.map.render_q10_map" ,
330+ side_effect = [b"map with dock" , b"map with docked robot" ],
331+ ) as render :
332+ trait .update_from_map_packet (packet )
333+ notified .clear ()
334+ map_dps .update_from_dps ({B01_Q10_DP .STATUS : YXDeviceState .CHARGING .code })
335+ map_dps .update_from_dps ({B01_Q10_DP .BATTERY : 50 })
336+
337+ assert trait .image_content == b"map with docked robot"
338+ assert trait .path == []
339+ assert notified == [None ]
340+ assert render .call_count == 2
341+ assert render .call_args .kwargs ["robot_at_dock" ] is True
342+
343+
344+ def test_entering_docked_state_clears_stale_live_trace () -> None :
345+ """A completed cleaning path cannot remain the caller-facing live position."""
346+ map_dps = MapDpsTrait ()
347+ trait = _map_trait (map_dps )
348+ trait .update_from_trace_packet (Q10TracePacket (points = [Q10Point (1 , 2 ), Q10Point (3 , 4 )]))
349+ notified : list [None ] = []
350+ trait .add_update_listener (lambda : notified .append (None ))
351+
352+ map_dps .update_from_dps ({B01_Q10_DP .STATUS : YXDeviceState .CHARGING .code })
353+
354+ assert trait .path == []
355+ assert trait .robot_position is None
356+ assert trait .robot_heading is None
357+ assert notified == [None ]
358+
359+
360+ def test_late_trace_does_not_move_docked_robot () -> None :
361+ """A delayed trace packet cannot revive a completed cleaning path."""
362+ map_dps = MapDpsTrait ()
363+ map_dps .update_from_dps ({B01_Q10_DP .STATUS : YXDeviceState .CHARGING .code })
364+ trait = _map_trait (map_dps )
365+
366+ trait .update_from_trace_packet (Q10TracePacket (points = [Q10Point (1 , 2 )]))
367+
368+ assert trait .path == []
369+ assert trait .robot_position is None
370+
371+
372+ def test_emptying_state_keeps_robot_at_dock () -> None :
373+ """Dock emptying must not briefly remove the docked robot marker."""
374+ map_dps = MapDpsTrait ()
375+ trait = _map_trait (map_dps )
376+ packet = parse_map_packet (FIXTURE .read_bytes ())
377+
378+ with patch (
379+ "roborock.devices.traits.b01.q10.map.render_q10_map" ,
380+ side_effect = [b"map with dock" , b"map while emptying" ],
381+ ) as render :
382+ trait .update_from_map_packet (packet )
383+ map_dps .update_from_dps ({B01_Q10_DP .STATUS : YXDeviceState .EMPTYING_THE_BIN .code })
384+
385+ assert trait .image_content == b"map while emptying"
386+ assert render .call_args .kwargs ["robot_at_dock" ] is True
387+
388+
389+ def test_combined_status_and_overlay_update_renders_once () -> None :
390+ """One map DPS update publishes the complete new rendering state."""
391+ map_dps = MapDpsTrait ()
392+ trait = _map_trait (map_dps )
393+ notified : list [None ] = []
394+ trait .add_update_listener (lambda : notified .append (None ))
395+
396+ with patch (
397+ "roborock.devices.traits.b01.q10.map.render_q10_map" ,
398+ side_effect = [b"base map" , b"combined map" ],
399+ ) as render :
400+ trait .update_from_map_packet (parse_map_packet (FIXTURE .read_bytes ()))
401+ notified .clear ()
402+ map_dps .update_from_dps (
403+ {
404+ B01_Q10_DP .STATUS : YXDeviceState .CHARGING .code ,
405+ B01_Q10_DP .RESTRICTED_ZONE_UP : _zone_blob (),
406+ }
407+ )
408+
409+ assert render .call_count == 2
410+ assert len (render .call_args .args [2 ].zones ) == 1
411+ assert render .call_args .kwargs ["robot_at_dock" ] is True
412+ assert notified == [None ]
413+ assert trait .image_content == b"combined map"
0 commit comments