"""Regression tests for the Plotly hover/snap-marker contract. ``plotly_hover``/``plotly_unhover`` fire continuously while the mouse moves. Touching the plot on each one made it redraw per mouse move, re-firing hover until the stack blew (``RangeError: Maximum call stack size exceeded``) and the canvas froze. The marker is therefore coalesced and only touched when its shown/hidden state actually changes. """ from __future__ import annotations from pathlib import Path import pytest pytest.importorskip("PySide6") pytest.importorskip("plotly") from otko.services import load_project EXAMPLES = Path(__file__).resolve().parents[2] / "examples" #: 200 mouse-move events over a *non-snap* point with snapping armed. _CHURN_PROBE = """(function(){ var gd = document.getElementById('plot'); var n = 0; var original = Plotly.restyle; Plotly.restyle = function () { n++; return original.apply(Plotly, arguments); }; var point = { data: gd.data[0], customdata: [0, 0, 0] }; try { for (var i = 0; i < 200; i++) { gd.emit('plotly_hover', { points: [point] }); gd.emit('plotly_unhover', {}); } return n; } finally { Plotly.restyle = original; } })()""" _HOVER_MARKER_X = ( "JSON.stringify((document.getElementById('plot').data" ".find(function(d){return d.meta && d.meta.kind === 'hover';}) || {}).x)" ) _EMIT_SNAP_HOVER = """(function(){ var gd = document.getElementById('plot'); var snap = gd.data.filter(function(d){return d.meta && d.meta.kind === 'snap';})[0]; var point = { data: snap, customdata: [snap.x[0], snap.y[0], snap.z[0]] }; gd.emit('plotly_hover', { points: [point] }); return true; })()""" def _open_canvas(qtbot, name: str): # type: ignore[no-untyped-def] from otko.views.canvas_plotly import PlotlyCanvas canvas = PlotlyCanvas() qtbot.addWidget(canvas) canvas.show_project(load_project(EXAMPLES / name)) qtbot.waitUntil(lambda: canvas._ready, timeout=30000) qtbot.wait(1500) # let the first Plotly.react settle return canvas def _run_js(canvas, qtbot, script: str, timeout: int = 15000): # type: ignore[no-untyped-def] box: dict[str, object] = {} canvas._web.page().runJavaScript(script, lambda value: box.update(value=value)) qtbot.waitUntil(lambda: "value" in box, timeout=timeout) return box["value"] @pytest.mark.gui def test_ordinary_mouse_movement_does_not_redraw(qtbot) -> None: # type: ignore[no-untyped-def] """The unfixed version restyled once per hover/unhover event.""" canvas = _open_canvas(qtbot, "space_frame_3d.osmodel") restyles = _run_js(canvas, qtbot, _CHURN_PROBE) assert restyles == 0, f"mouse movement caused {restyles} plot restyles" @pytest.mark.gui def test_snap_marker_shows_and_clears(qtbot) -> None: # type: ignore[no-untyped-def] canvas = _open_canvas(qtbot, "basic_truss.osmodel") assert _run_js(canvas, qtbot, _HOVER_MARKER_X) == "[]" canvas.set_snap_preview_enabled(True) qtbot.wait(100) _run_js(canvas, qtbot, _EMIT_SNAP_HOVER) qtbot.wait(250) assert _run_js(canvas, qtbot, _HOVER_MARKER_X) != "[]", "snap target not shown" _run_js(canvas, qtbot, "document.getElementById('plot').emit('plotly_unhover',{})") qtbot.wait(250) assert _run_js(canvas, qtbot, _HOVER_MARKER_X) == "[]", "snap target not cleared"