mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-18 03:54:17 +02:00
- Added frontnend/backend tests for printer controls
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user