- Added frontnend/backend tests for printer controls

This commit is contained in:
maziggy
2026-01-01 16:02:25 +01:00
parent f7994df84f
commit fe250b9b7f
9 changed files with 425 additions and 108 deletions
+22
View File
@@ -2,6 +2,28 @@
All notable changes to Bambuddy will be documented in this file.
## [0.1.6b5] - 2026-01-01
### Added
- **Printer control buttons** - Stop and Pause/Resume buttons on printer cards when printing:
- Stop button cancels the current print job
- Pause/Resume toggle for pausing and resuming prints
- Confirmation modals for all actions to prevent accidental clicks
- Toast notifications for action feedback
- **AMS slot RFID re-read** - Re-read RFID data for individual AMS slots:
- Menu button (⋮) appears on hover over AMS slots
- "Re-read RFID" option triggers filament info refresh
- Loading indicator shows while re-read is in progress
- Automatically tracks printer status to clear indicator when complete
- Menu hidden when printer is busy (printing)
### Changed
- **Temperature cards layout** - Refactored printer card layout with slimmer temperature displays to make room for control buttons
### Tests
- Added integration tests for printer control endpoints (stop, pause, resume)
- Added integration tests for AMS slot refresh endpoint
## [0.1.6b4] - 2026-01-01
### Added
+3 -1
View File
@@ -52,9 +52,11 @@
- Re-print to any connected printer with AMS filament preview
- Archive comparison (side-by-side diff)
### 📊 Monitoring & Stats
### 📊 Monitoring & Control
- Real-time printer status via WebSocket
- Live camera streaming (MJPEG) & snapshots
- Printer control (stop, pause, resume)
- AMS slot RFID re-read
- HMS error monitoring with history
- Print success rates & trends
- Filament usage tracking
+91
View File
@@ -1011,3 +1011,94 @@ async def debug_simulate_print_complete(
await on_print_complete(printer_id, data)
return {"success": True, "archive_id": archive.id, "message": "Print completion simulated"}
# =============================================================================
# Print Control Endpoints
# =============================================================================
@router.post("/{printer_id}/print/stop")
async def stop_print(printer_id: int, db: AsyncSession = Depends(get_db)):
"""Stop/cancel the current print job."""
result = await db.execute(select(Printer).where(Printer.id == printer_id))
printer = result.scalar_one_or_none()
if not printer:
raise HTTPException(404, "Printer not found")
client = printer_manager.get_client(printer_id)
if not client:
raise HTTPException(400, "Printer not connected")
success = client.stop_print()
if not success:
raise HTTPException(500, "Failed to stop print")
return {"success": True, "message": "Print stop command sent"}
@router.post("/{printer_id}/print/pause")
async def pause_print(printer_id: int, db: AsyncSession = Depends(get_db)):
"""Pause the current print job."""
result = await db.execute(select(Printer).where(Printer.id == printer_id))
printer = result.scalar_one_or_none()
if not printer:
raise HTTPException(404, "Printer not found")
client = printer_manager.get_client(printer_id)
if not client:
raise HTTPException(400, "Printer not connected")
success = client.pause_print()
if not success:
raise HTTPException(500, "Failed to pause print")
return {"success": True, "message": "Print pause command sent"}
@router.post("/{printer_id}/print/resume")
async def resume_print(printer_id: int, db: AsyncSession = Depends(get_db)):
"""Resume a paused print job."""
result = await db.execute(select(Printer).where(Printer.id == printer_id))
printer = result.scalar_one_or_none()
if not printer:
raise HTTPException(404, "Printer not found")
client = printer_manager.get_client(printer_id)
if not client:
raise HTTPException(400, "Printer not connected")
success = client.resume_print()
if not success:
raise HTTPException(500, "Failed to resume print")
return {"success": True, "message": "Print resume command sent"}
# =============================================================================
# AMS Control Endpoints
# =============================================================================
@router.post("/{printer_id}/ams/{ams_id}/slot/{slot_id}/refresh")
async def refresh_ams_slot(
printer_id: int,
ams_id: int,
slot_id: int,
db: AsyncSession = Depends(get_db),
):
"""Re-read RFID for an AMS slot (triggers filament info refresh)."""
result = await db.execute(select(Printer).where(Printer.id == printer_id))
printer = result.scalar_one_or_none()
if not printer:
raise HTTPException(404, "Printer not found")
client = printer_manager.get_client(printer_id)
if not client:
raise HTTPException(400, "Printer not connected")
success, message = client.ams_refresh_tray(ams_id, slot_id)
if not success:
raise HTTPException(400, message)
return {"success": True, "message": message}
+1 -1
View File
@@ -5,7 +5,7 @@ from pathlib import Path
from pydantic_settings import BaseSettings
# Application version - single source of truth
APP_VERSION = "0.1.6b4"
APP_VERSION = "0.1.6b5"
GITHUB_REPO = "maziggy/bambuddy"
# App directory - where the application is installed (for static files)
@@ -291,3 +291,197 @@ class TestPrinterDataIntegrity:
assert response.status_code == 200
assert response.json()["status"] == "refresh_requested"
mock_pm.request_status_update.assert_called_once_with(printer.id)
class TestPrintControlAPI:
"""Integration tests for print control endpoints (stop, pause, resume)."""
# ========================================================================
# Stop print endpoint
# ========================================================================
@pytest.mark.asyncio
@pytest.mark.integration
async def test_stop_print_not_found(self, async_client: AsyncClient):
"""Verify 404 for non-existent printer."""
response = await async_client.post("/api/v1/printers/99999/print/stop")
assert response.status_code == 404
@pytest.mark.asyncio
@pytest.mark.integration
async def test_stop_print_not_connected(self, async_client: AsyncClient, printer_factory):
"""Verify error when printer is not connected."""
printer = await printer_factory(name="Disconnected Printer")
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}/print/stop")
assert response.status_code == 400
assert "not connected" in response.json()["detail"].lower()
@pytest.mark.asyncio
@pytest.mark.integration
async def test_stop_print_success(self, async_client: AsyncClient, printer_factory):
"""Verify successful stop print request."""
printer = await printer_factory(name="Printing Printer")
mock_client = MagicMock()
mock_client.stop_print.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}/print/stop")
assert response.status_code == 200
assert response.json()["success"] is True
mock_client.stop_print.assert_called_once()
# ========================================================================
# Pause print endpoint
# ========================================================================
@pytest.mark.asyncio
@pytest.mark.integration
async def test_pause_print_not_found(self, async_client: AsyncClient):
"""Verify 404 for non-existent printer."""
response = await async_client.post("/api/v1/printers/99999/print/pause")
assert response.status_code == 404
@pytest.mark.asyncio
@pytest.mark.integration
async def test_pause_print_not_connected(self, async_client: AsyncClient, printer_factory):
"""Verify error when printer is not connected."""
printer = await printer_factory(name="Disconnected Printer")
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}/print/pause")
assert response.status_code == 400
assert "not connected" in response.json()["detail"].lower()
@pytest.mark.asyncio
@pytest.mark.integration
async def test_pause_print_success(self, async_client: AsyncClient, printer_factory):
"""Verify successful pause print request."""
printer = await printer_factory(name="Printing Printer")
mock_client = MagicMock()
mock_client.pause_print.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}/print/pause")
assert response.status_code == 200
assert response.json()["success"] is True
mock_client.pause_print.assert_called_once()
# ========================================================================
# Resume print endpoint
# ========================================================================
@pytest.mark.asyncio
@pytest.mark.integration
async def test_resume_print_not_found(self, async_client: AsyncClient):
"""Verify 404 for non-existent printer."""
response = await async_client.post("/api/v1/printers/99999/print/resume")
assert response.status_code == 404
@pytest.mark.asyncio
@pytest.mark.integration
async def test_resume_print_not_connected(self, async_client: AsyncClient, printer_factory):
"""Verify error when printer is not connected."""
printer = await printer_factory(name="Disconnected Printer")
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}/print/resume")
assert response.status_code == 400
assert "not connected" in response.json()["detail"].lower()
@pytest.mark.asyncio
@pytest.mark.integration
async def test_resume_print_success(self, async_client: AsyncClient, printer_factory):
"""Verify successful resume print request."""
printer = await printer_factory(name="Paused Printer")
mock_client = MagicMock()
mock_client.resume_print.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}/print/resume")
assert response.status_code == 200
assert response.json()["success"] is True
mock_client.resume_print.assert_called_once()
class TestAMSRefreshAPI:
"""Integration tests for AMS slot refresh endpoint."""
@pytest.mark.asyncio
@pytest.mark.integration
async def test_ams_refresh_not_found(self, async_client: AsyncClient):
"""Verify 404 for non-existent printer."""
response = await async_client.post("/api/v1/printers/99999/ams/0/slot/0/refresh")
assert response.status_code == 404
@pytest.mark.asyncio
@pytest.mark.integration
async def test_ams_refresh_not_connected(self, async_client: AsyncClient, printer_factory):
"""Verify error when printer is not connected."""
printer = await printer_factory(name="Disconnected Printer")
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}/ams/0/slot/0/refresh")
assert response.status_code == 400
assert "not connected" in response.json()["detail"].lower()
@pytest.mark.asyncio
@pytest.mark.integration
async def test_ams_refresh_success(self, async_client: AsyncClient, printer_factory):
"""Verify successful AMS refresh request."""
printer = await printer_factory(name="Printer with AMS")
mock_client = MagicMock()
mock_client.ams_refresh_tray.return_value = (True, "Refreshing AMS 0 tray 1")
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}/ams/0/slot/1/refresh")
assert response.status_code == 200
result = response.json()
assert result["success"] is True
mock_client.ams_refresh_tray.assert_called_once_with(0, 1)
@pytest.mark.asyncio
@pytest.mark.integration
async def test_ams_refresh_filament_loaded(self, async_client: AsyncClient, printer_factory):
"""Verify error when filament is loaded (can't refresh while loaded)."""
printer = await printer_factory(name="Printer with AMS")
mock_client = MagicMock()
mock_client.ams_refresh_tray.return_value = (False, "Please unload filament first")
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}/ams/0/slot/0/refresh")
assert response.status_code == 400
assert "unload" in response.json()["detail"].lower()
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -23,8 +23,8 @@
<!-- Splash screens for iOS -->
<link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
<script type="module" crossorigin src="/assets/index-3umWYOC3.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CCbBv2VC.css">
<script type="module" crossorigin src="/assets/index-3UvyJ1HQ.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BJk0xs4c.css">
</head>
<body>
<div id="root"></div>