mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-04 13:11:35 +02:00
Adds a compact "Bed" badge in the printer-card controls row
between print speed and Stop/Pause. Opens a popover with up/down
arrows and a 1 / 10 / 50 mm step selector.
When the Z axis has not been homed since the last print, the
first jog per session opens a Bambu Studio-style modal with
Home Z / Move anyway / Cancel. "Move anyway" bypasses soft
endstops (M211 S0 ... M211 S1) for a single move and is
remembered for the browser session.
Backend:
- POST /printers/{id}/bed-jog?distance=N[&force=bool]
Emits G91 / G1 ZN F600 / G90 (with optional M211 wrap).
Distance validated server-side (non-zero, |N| <= 200 mm).
- POST /printers/{id}/home-axes?axes=z|xy|all
Emits G28 variants.
Both gated behind Permission.PRINTERS_CONTROL.
Frontend:
- New indigo-themed badge + popover in PrintersPage.
- Not-homed confirmation modal with sessionStorage "warned" flag.
- i18n keys under printers.bedJog.* in all 7 locales.
Tests:
- backend/tests/unit/test_bed_jog.py — 13 tests covering
404 / 400 / 500 / success paths for both endpoints, plus
gcode-payload assertions for force on/off.
Docs:
- README feature list, CHANGELOG (0.2.3b4 Unreleased),
printer-control wiki page, website features.html.
119 lines
5.7 KiB
Python
119 lines
5.7 KiB
Python
"""Unit tests for the bed-jog and home-axes endpoints (#791).
|
|
|
|
Tests:
|
|
POST /api/v1/printers/{printer_id}/bed-jog?distance=<mm>&force=<bool>
|
|
POST /api/v1/printers/{printer_id}/home-axes?axes=<z|xy|all>
|
|
"""
|
|
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
from httpx import AsyncClient
|
|
|
|
|
|
class TestBedJogAPI:
|
|
@pytest.mark.asyncio
|
|
async def test_bed_jog_not_found(self, async_client: AsyncClient):
|
|
response = await async_client.post("/api/v1/printers/99999/bed-jog?distance=10")
|
|
assert response.status_code == 404
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_bed_jog_zero_distance_rejected(self, async_client: AsyncClient, printer_factory):
|
|
printer = await printer_factory(name="P1")
|
|
response = await async_client.post(f"/api/v1/printers/{printer.id}/bed-jog?distance=0")
|
|
assert response.status_code == 400
|
|
assert "distance" in response.json()["detail"].lower()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_bed_jog_too_large_rejected(self, async_client: AsyncClient, printer_factory):
|
|
printer = await printer_factory(name="P1")
|
|
response = await async_client.post(f"/api/v1/printers/{printer.id}/bed-jog?distance=500")
|
|
assert response.status_code == 400
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_bed_jog_not_connected(self, async_client: AsyncClient, printer_factory):
|
|
printer = await printer_factory(name="Disconnected")
|
|
with patch("backend.app.api.routes.printers.printer_manager") as mock_pm:
|
|
mock_pm.get_client.return_value = None
|
|
response = await async_client.post(f"/api/v1/printers/{printer.id}/bed-jog?distance=10")
|
|
assert response.status_code == 400
|
|
assert "not connected" in response.json()["detail"].lower()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_bed_jog_send_failure(self, async_client: AsyncClient, printer_factory):
|
|
printer = await printer_factory(name="P1")
|
|
mock_client = MagicMock()
|
|
mock_client.send_gcode.return_value = False
|
|
with patch("backend.app.api.routes.printers.printer_manager") as mock_pm:
|
|
mock_pm.get_client.return_value = mock_client
|
|
response = await async_client.post(f"/api/v1/printers/{printer.id}/bed-jog?distance=10")
|
|
assert response.status_code == 500
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_bed_jog_success_without_force(self, async_client: AsyncClient, printer_factory):
|
|
"""When force=false the M211 guard lines must not be emitted."""
|
|
printer = await printer_factory(name="P1")
|
|
mock_client = MagicMock()
|
|
mock_client.send_gcode.return_value = True
|
|
with patch("backend.app.api.routes.printers.printer_manager") as mock_pm:
|
|
mock_pm.get_client.return_value = mock_client
|
|
response = await async_client.post(f"/api/v1/printers/{printer.id}/bed-jog?distance=10&force=false")
|
|
assert response.status_code == 200
|
|
sent_gcode = mock_client.send_gcode.call_args[0][0]
|
|
assert "G91" in sent_gcode
|
|
assert "G1 Z10.00" in sent_gcode
|
|
assert "G90" in sent_gcode
|
|
assert "M211" not in sent_gcode
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_bed_jog_success_with_force(self, async_client: AsyncClient, printer_factory):
|
|
"""force=true must wrap the move in M211 S0 / M211 S1."""
|
|
printer = await printer_factory(name="P1")
|
|
mock_client = MagicMock()
|
|
mock_client.send_gcode.return_value = True
|
|
with patch("backend.app.api.routes.printers.printer_manager") as mock_pm:
|
|
mock_pm.get_client.return_value = mock_client
|
|
response = await async_client.post(f"/api/v1/printers/{printer.id}/bed-jog?distance=-5&force=true")
|
|
assert response.status_code == 200
|
|
sent_gcode = mock_client.send_gcode.call_args[0][0]
|
|
lines = sent_gcode.splitlines()
|
|
assert lines[0] == "M211 S0"
|
|
assert lines[-1] == "M211 S1"
|
|
assert "G1 Z-5.00" in sent_gcode
|
|
|
|
|
|
class TestHomeAxesAPI:
|
|
@pytest.mark.asyncio
|
|
async def test_home_axes_not_found(self, async_client: AsyncClient):
|
|
response = await async_client.post("/api/v1/printers/99999/home-axes?axes=z")
|
|
assert response.status_code == 404
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_home_axes_invalid(self, async_client: AsyncClient, printer_factory):
|
|
printer = await printer_factory(name="P1")
|
|
response = await async_client.post(f"/api/v1/printers/{printer.id}/home-axes?axes=bogus")
|
|
assert response.status_code == 400
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize(
|
|
"axes,expected",
|
|
[("z", "G28 Z"), ("xy", "G28 X Y"), ("all", "G28")],
|
|
)
|
|
async def test_home_axes_success(self, async_client: AsyncClient, printer_factory, axes, expected):
|
|
printer = await printer_factory(name="P1")
|
|
mock_client = MagicMock()
|
|
mock_client.send_gcode.return_value = True
|
|
with patch("backend.app.api.routes.printers.printer_manager") as mock_pm:
|
|
mock_pm.get_client.return_value = mock_client
|
|
response = await async_client.post(f"/api/v1/printers/{printer.id}/home-axes?axes={axes}")
|
|
assert response.status_code == 200
|
|
mock_client.send_gcode.assert_called_once_with(expected)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_home_axes_not_connected(self, async_client: AsyncClient, printer_factory):
|
|
printer = await printer_factory(name="D")
|
|
with patch("backend.app.api.routes.printers.printer_manager") as mock_pm:
|
|
mock_pm.get_client.return_value = None
|
|
response = await async_client.post(f"/api/v1/printers/{printer.id}/home-axes?axes=z")
|
|
assert response.status_code == 400
|