Plotly.react resets any scene attribute the incoming layout omits, so every selection change snapped the camera back to the default and re-fit the ranges - the pinned framing was meaningless. Data-only pushes now pass preserveView and html.py merges the live camera and axis ranges into the incoming layout; only an explicit re-frame sends computed framing. Also fixes the ordering bug where the framing flag was consumed before the not-ready early return, which dropped the very first framing on the floor (show_project runs before loadFinished). Verified in the browser: user orbit + zoom survive selection and working plane updates, while reset_camera re-frames. Regression-tested headlessly by asserting the emitted preserveView flag.
97 lines
3.4 KiB
Python
97 lines
3.4 KiB
Python
"""Regression tests for the Plotly canvas push contract.
|
|
|
|
``Plotly.react`` resets any scene attribute the incoming layout omits, so a
|
|
data-only push must be flagged ``preserveView`` (the JS side then carries the
|
|
live camera and axis ranges forward). Only explicit re-frames may send the
|
|
computed framing. These tests capture the JavaScript the canvas emits instead
|
|
of driving the browser, so they stay fast and deterministic.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
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"
|
|
|
|
|
|
def _canvas_with_captured_js(qtbot): # type: ignore[no-untyped-def]
|
|
from otko.views.canvas_plotly import PlotlyCanvas
|
|
|
|
canvas = PlotlyCanvas()
|
|
qtbot.addWidget(canvas)
|
|
# Pretend the page finished loading, then capture instead of running JS.
|
|
canvas._ready = True
|
|
calls: list[str] = []
|
|
canvas._eval = lambda js: calls.append(js) # type: ignore[method-assign]
|
|
return canvas, calls
|
|
|
|
|
|
def _parse_call(call: str) -> tuple[dict, bool]:
|
|
"""Split a captured ``window.otkoUpdate(<json>, <flag>)`` call."""
|
|
body = call[len("window.otkoUpdate(") : -1]
|
|
literal, _sep, flag = body.rpartition(", ")
|
|
return json.loads(json.loads(literal)), flag == "true"
|
|
|
|
|
|
@pytest.mark.gui
|
|
def test_framing_push_is_not_marked_preserve_view(qtbot) -> None: # type: ignore[no-untyped-def]
|
|
canvas, calls = _canvas_with_captured_js(qtbot)
|
|
|
|
canvas.show_project(load_project(EXAMPLES / "cantilever.osmodel"))
|
|
|
|
assert calls, "show_project must push a figure"
|
|
payload, preserve = _parse_call(calls[-1])
|
|
assert preserve is False, "initial push must be a re-frame"
|
|
# The framing push carries the camera and the padded ranges.
|
|
scene = payload["layout"]["scene"]
|
|
assert "camera" in scene
|
|
assert "range" in scene["xaxis"]
|
|
assert scene["xaxis"]["autorange"] is False
|
|
|
|
|
|
@pytest.mark.gui
|
|
def test_data_update_preserves_the_view(qtbot) -> None: # type: ignore[no-untyped-def]
|
|
canvas, calls = _canvas_with_captured_js(qtbot)
|
|
|
|
canvas.show_project(load_project(EXAMPLES / "cantilever.osmodel"))
|
|
calls.clear()
|
|
|
|
canvas.selection.select_node(1) # data-only update
|
|
|
|
assert calls, "selection change must push"
|
|
payload, preserve = _parse_call(calls[-1])
|
|
assert preserve is True, "data update must preserve the view"
|
|
assert "camera" not in payload["layout"]["scene"], "must not re-send the camera"
|
|
|
|
|
|
@pytest.mark.gui
|
|
def test_view_presets_and_reset_re_frame(qtbot) -> None: # type: ignore[no-untyped-def]
|
|
canvas, calls = _canvas_with_captured_js(qtbot)
|
|
|
|
canvas.show_project(load_project(EXAMPLES / "cantilever.osmodel"))
|
|
|
|
for action in (canvas.view_xy, canvas.view_xz, canvas.view_yz, canvas.reset_camera):
|
|
calls.clear()
|
|
action()
|
|
assert calls[-1].endswith(", false)"), f"{action.__name__} must re-frame"
|
|
|
|
|
|
@pytest.mark.gui
|
|
def test_style_change_re_frames_for_layout_only_attrs(qtbot) -> None: # type: ignore[no-untyped-def]
|
|
from otko.views.canvas3d.style import RenderStyle
|
|
|
|
canvas, calls = _canvas_with_captured_js(qtbot)
|
|
canvas.show_project(load_project(EXAMPLES / "cantilever.osmodel"))
|
|
calls.clear()
|
|
|
|
canvas.set_style(RenderStyle(show_axis_outline=True))
|
|
|
|
assert calls[-1].endswith(", false)"), "background/outline live in the layout"
|