diff --git a/backend/app/services/spoolman.py b/backend/app/services/spoolman.py
index f95638b7b..52190e645 100644
--- a/backend/app/services/spoolman.py
+++ b/backend/app/services/spoolman.py
@@ -595,7 +595,7 @@ class SpoolmanClient:
filament_id=filament["id"],
remaining_weight=remaining,
location=location,
- comment=f"Auto-created from {printer_name} AMS",
+ comment="Created by Bambuddy",
extra={"tag": json.dumps(tray.tray_uuid)},
)
diff --git a/backend/tests/integration/test_spoolman_api.py b/backend/tests/integration/test_spoolman_api.py
new file mode 100644
index 000000000..f22c54bad
--- /dev/null
+++ b/backend/tests/integration/test_spoolman_api.py
@@ -0,0 +1,423 @@
+"""Integration tests for Spoolman API endpoints."""
+
+from unittest.mock import AsyncMock, MagicMock, patch
+
+import pytest
+from httpx import AsyncClient
+
+
+class TestSpoolmanAPI:
+ """Integration tests for /api/v1/spoolman/ endpoints."""
+
+ @pytest.fixture
+ async def spoolman_settings(self, db_session):
+ """Create Spoolman settings in the database (enabled with URL)."""
+ from backend.app.models.settings import Settings
+
+ # Both settings are required for Spoolman to work
+ enabled_setting = Settings(key="spoolman_enabled", value="true")
+ url_setting = Settings(key="spoolman_url", value="http://localhost:7912")
+ db_session.add(enabled_setting)
+ db_session.add(url_setting)
+ await db_session.commit()
+ return {"enabled": enabled_setting, "url": url_setting}
+
+ @pytest.fixture
+ async def spoolman_url_only(self, db_session):
+ """Create only the URL setting (not enabled)."""
+ from backend.app.models.settings import Settings
+
+ setting = Settings(key="spoolman_url", value="http://localhost:7912")
+ db_session.add(setting)
+ await db_session.commit()
+ return setting
+
+ @pytest.fixture
+ def mock_spoolman_client(self):
+ """Mock the Spoolman client functions."""
+ mock_client = MagicMock()
+ mock_client.is_connected = True
+ mock_client.base_url = "http://localhost:7912"
+ mock_client.health_check = AsyncMock(return_value=True)
+ mock_client.get_spools = AsyncMock(return_value=[])
+ mock_client.get_filaments = AsyncMock(return_value=[])
+ mock_client.create_spool = AsyncMock(return_value={"id": 1})
+ mock_client.update_spool = AsyncMock(return_value={"id": 1})
+ mock_client.close = AsyncMock()
+
+ with (
+ patch(
+ "backend.app.api.routes.spoolman.get_spoolman_client",
+ AsyncMock(return_value=mock_client),
+ ),
+ patch(
+ "backend.app.api.routes.spoolman.init_spoolman_client",
+ AsyncMock(return_value=mock_client),
+ ),
+ patch(
+ "backend.app.api.routes.spoolman.close_spoolman_client",
+ AsyncMock(),
+ ),
+ ):
+ yield mock_client
+
+ @pytest.fixture
+ def mock_spoolman_disconnected(self):
+ """Mock the Spoolman client as disconnected (returns None)."""
+ with (
+ patch(
+ "backend.app.api.routes.spoolman.get_spoolman_client",
+ AsyncMock(return_value=None),
+ ),
+ patch(
+ "backend.app.api.routes.spoolman.init_spoolman_client",
+ AsyncMock(return_value=None),
+ ),
+ ):
+ yield
+
+ # =========================================================================
+ # Status Endpoint Tests
+ # =========================================================================
+
+ @pytest.mark.asyncio
+ @pytest.mark.integration
+ async def test_get_status_not_configured(self, async_client: AsyncClient):
+ """Verify status shows not enabled when no settings exist."""
+ response = await async_client.get("/api/v1/spoolman/status")
+ assert response.status_code == 200
+ data = response.json()
+ assert data["enabled"] is False
+ assert data["connected"] is False
+
+ @pytest.mark.asyncio
+ @pytest.mark.integration
+ async def test_get_status_url_only_not_enabled(self, async_client: AsyncClient, spoolman_url_only):
+ """Verify status shows not enabled when only URL is set."""
+ response = await async_client.get("/api/v1/spoolman/status")
+ assert response.status_code == 200
+ data = response.json()
+ assert data["enabled"] is False
+ assert data["url"] == "http://localhost:7912"
+
+ @pytest.mark.asyncio
+ @pytest.mark.integration
+ async def test_get_status_enabled_and_connected(
+ self, async_client: AsyncClient, spoolman_settings, mock_spoolman_client
+ ):
+ """Verify status shows enabled and connected when properly configured."""
+ response = await async_client.get("/api/v1/spoolman/status")
+ assert response.status_code == 200
+ data = response.json()
+ assert data["enabled"] is True
+ assert data["connected"] is True
+ assert data["url"] == "http://localhost:7912"
+
+ # =========================================================================
+ # Connect/Disconnect Tests
+ # =========================================================================
+
+ @pytest.mark.asyncio
+ @pytest.mark.integration
+ async def test_connect_not_enabled(self, async_client: AsyncClient):
+ """Verify connect fails when not enabled."""
+ response = await async_client.post("/api/v1/spoolman/connect")
+ assert response.status_code == 400
+ assert "not enabled" in response.json()["detail"].lower()
+
+ @pytest.mark.asyncio
+ @pytest.mark.integration
+ async def test_connect_success(self, async_client: AsyncClient, spoolman_settings, mock_spoolman_client):
+ """Verify successful connection to Spoolman."""
+ response = await async_client.post("/api/v1/spoolman/connect")
+ assert response.status_code == 200
+ data = response.json()
+ assert data["success"] is True
+ assert "connected" in data["message"].lower()
+
+ @pytest.mark.asyncio
+ @pytest.mark.integration
+ async def test_disconnect(self, async_client: AsyncClient, spoolman_settings, mock_spoolman_client):
+ """Verify disconnect works."""
+ response = await async_client.post("/api/v1/spoolman/disconnect")
+ assert response.status_code == 200
+ assert "disconnected" in response.json()["message"].lower()
+
+ # =========================================================================
+ # Spools Endpoint Tests
+ # =========================================================================
+
+ @pytest.mark.asyncio
+ @pytest.mark.integration
+ async def test_get_spools_not_enabled(self, async_client: AsyncClient):
+ """Verify get spools fails when not enabled."""
+ response = await async_client.get("/api/v1/spoolman/spools")
+ assert response.status_code == 400
+ assert "not enabled" in response.json()["detail"].lower()
+
+ @pytest.mark.asyncio
+ @pytest.mark.integration
+ async def test_get_spools_success(self, async_client: AsyncClient, spoolman_settings, mock_spoolman_client):
+ """Verify get spools returns data in expected format."""
+ mock_spool = {
+ "id": 1,
+ "remaining_weight": 500,
+ "used_weight": 500,
+ "filament": {
+ "id": 1,
+ "name": "PLA Basic",
+ "material": "PLA",
+ "color_hex": "FF0000",
+ },
+ "first_used": "2024-01-01",
+ "last_used": "2024-01-15",
+ "location": "AMS1",
+ "lot_nr": "LOT123",
+ "comment": "Test spool",
+ "extra": {"tag": '"ABC123"'},
+ }
+ mock_spoolman_client.get_spools = AsyncMock(return_value=[mock_spool])
+
+ response = await async_client.get("/api/v1/spoolman/spools")
+ assert response.status_code == 200
+ data = response.json()
+ assert "spools" in data
+ assert isinstance(data["spools"], list)
+ assert len(data["spools"]) == 1
+ assert data["spools"][0]["id"] == 1
+
+ # =========================================================================
+ # Unlinked Spools Tests
+ # =========================================================================
+
+ @pytest.mark.asyncio
+ @pytest.mark.integration
+ async def test_get_unlinked_spools_not_enabled(self, async_client: AsyncClient):
+ """Verify get unlinked spools fails when not enabled."""
+ response = await async_client.get("/api/v1/spoolman/spools/unlinked")
+ assert response.status_code == 400
+
+ @pytest.mark.asyncio
+ @pytest.mark.integration
+ async def test_get_unlinked_spools_success(
+ self, async_client: AsyncClient, spoolman_settings, mock_spoolman_client
+ ):
+ """Verify get unlinked spools returns spools without tags."""
+ # Mock spool without extra.tag (unlinked)
+ mock_spool = {
+ "id": 1,
+ "remaining_weight": 800,
+ "used_weight": 200,
+ "extra": {}, # No tag = unlinked
+ "filament": {
+ "id": 1,
+ "name": "PLA Basic",
+ "material": "PLA",
+ "color_hex": "FF0000",
+ },
+ "location": "Shelf A",
+ }
+ mock_spoolman_client.get_spools = AsyncMock(return_value=[mock_spool])
+
+ response = await async_client.get("/api/v1/spoolman/spools/unlinked")
+ assert response.status_code == 200
+ data = response.json()
+ assert isinstance(data, list)
+ assert len(data) == 1
+ assert data[0]["id"] == 1
+ assert data[0]["filament_name"] == "PLA Basic"
+
+ @pytest.mark.asyncio
+ @pytest.mark.integration
+ async def test_get_unlinked_spools_excludes_linked(
+ self, async_client: AsyncClient, spoolman_settings, mock_spoolman_client
+ ):
+ """Verify linked spools (with tag) are excluded."""
+ # Mock spool with extra.tag (linked)
+ mock_spool_linked = {
+ "id": 1,
+ "remaining_weight": 800,
+ "used_weight": 200,
+ "extra": {"tag": '"ABC123"'}, # Has tag = linked
+ "filament": {"id": 1, "name": "PLA Red", "material": "PLA", "color_hex": "FF0000"},
+ }
+
+ # Mock spool without tag (unlinked)
+ mock_spool_unlinked = {
+ "id": 2,
+ "remaining_weight": 900,
+ "used_weight": 100,
+ "extra": {}, # No tag = unlinked
+ "filament": {"id": 2, "name": "PLA Blue", "material": "PLA", "color_hex": "0000FF"},
+ }
+
+ mock_spoolman_client.get_spools = AsyncMock(return_value=[mock_spool_linked, mock_spool_unlinked])
+
+ response = await async_client.get("/api/v1/spoolman/spools/unlinked")
+ assert response.status_code == 200
+ data = response.json()
+ assert len(data) == 1
+ assert data[0]["id"] == 2 # Only unlinked spool
+
+ # =========================================================================
+ # Link Spool Tests
+ # =========================================================================
+
+ @pytest.mark.asyncio
+ @pytest.mark.integration
+ async def test_link_spool_not_enabled(self, async_client: AsyncClient):
+ """Verify link spool fails when not enabled."""
+ response = await async_client.post(
+ "/api/v1/spoolman/spools/1/link",
+ json={"tray_uuid": "A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4"},
+ )
+ assert response.status_code == 400
+
+ @pytest.mark.asyncio
+ @pytest.mark.integration
+ async def test_link_spool_invalid_uuid_length(
+ self, async_client: AsyncClient, spoolman_settings, mock_spoolman_client
+ ):
+ """Verify link spool fails with invalid UUID length."""
+ response = await async_client.post(
+ "/api/v1/spoolman/spools/1/link",
+ json={"tray_uuid": "ABC123"}, # Too short
+ )
+ assert response.status_code == 400
+ assert "32 hex characters" in response.json()["detail"]
+
+ @pytest.mark.asyncio
+ @pytest.mark.integration
+ async def test_link_spool_invalid_uuid_format(
+ self, async_client: AsyncClient, spoolman_settings, mock_spoolman_client
+ ):
+ """Verify link spool fails with non-hex UUID."""
+ response = await async_client.post(
+ "/api/v1/spoolman/spools/1/link",
+ json={"tray_uuid": "ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ"}, # Not hex
+ )
+ assert response.status_code == 400
+ assert "hex" in response.json()["detail"].lower()
+
+ @pytest.mark.asyncio
+ @pytest.mark.integration
+ async def test_link_spool_success(self, async_client: AsyncClient, spoolman_settings, mock_spoolman_client):
+ """Verify successfully linking a spool to AMS tray."""
+ mock_spoolman_client.update_spool = AsyncMock(
+ return_value={"id": 1, "extra": {"tag": '"A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4"'}}
+ )
+
+ response = await async_client.post(
+ "/api/v1/spoolman/spools/1/link",
+ json={"tray_uuid": "A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4"},
+ )
+ assert response.status_code == 200
+ data = response.json()
+ assert data["success"] is True
+ assert "linked" in data["message"].lower()
+
+ # Verify update_spool was called
+ mock_spoolman_client.update_spool.assert_called_once()
+
+ # =========================================================================
+ # Sync Tests
+ # =========================================================================
+
+ @pytest.mark.asyncio
+ @pytest.mark.integration
+ async def test_sync_printer_not_enabled(self, async_client: AsyncClient, printer_factory):
+ """Verify sync fails when Spoolman not enabled."""
+ printer = await printer_factory()
+ response = await async_client.post(f"/api/v1/spoolman/sync/{printer.id}")
+ assert response.status_code == 400
+ assert "not enabled" in response.json()["detail"].lower()
+
+ @pytest.mark.asyncio
+ @pytest.mark.integration
+ async def test_sync_printer_not_found(self, async_client: AsyncClient, spoolman_settings, mock_spoolman_client):
+ """Verify sync fails for non-existent printer."""
+ response = await async_client.post("/api/v1/spoolman/sync/9999")
+ assert response.status_code == 404
+
+ @pytest.mark.asyncio
+ @pytest.mark.integration
+ async def test_sync_returns_result_structure(
+ self,
+ async_client: AsyncClient,
+ spoolman_settings,
+ mock_spoolman_client,
+ printer_factory,
+ ):
+ """Verify sync returns proper result structure."""
+ printer = await printer_factory()
+
+ # Mock printer manager to return AMS data
+ with patch("backend.app.api.routes.spoolman.printer_manager") as pm_mock:
+ mock_state = MagicMock()
+ mock_state.raw_data = {"ams": [{"id": 0, "tray": []}]}
+ pm_mock.get_status = MagicMock(return_value=mock_state)
+
+ response = await async_client.post(f"/api/v1/spoolman/sync/{printer.id}")
+ assert response.status_code == 200
+ data = response.json()
+ # Verify SyncResult structure
+ assert "success" in data
+ assert "synced_count" in data
+ assert "skipped_count" in data
+ assert "skipped" in data
+ assert "errors" in data
+ assert isinstance(data["skipped"], list)
+ assert isinstance(data["errors"], list)
+
+ @pytest.mark.asyncio
+ @pytest.mark.integration
+ async def test_sync_printer_not_connected(
+ self,
+ async_client: AsyncClient,
+ spoolman_settings,
+ mock_spoolman_client,
+ printer_factory,
+ ):
+ """Verify sync fails when printer is not connected (no status)."""
+ printer = await printer_factory()
+
+ with patch("backend.app.api.routes.spoolman.printer_manager") as pm_mock:
+ pm_mock.get_status = MagicMock(return_value=None)
+
+ response = await async_client.post(f"/api/v1/spoolman/sync/{printer.id}")
+ assert response.status_code == 404
+ assert "not connected" in response.json()["detail"].lower()
+
+ # =========================================================================
+ # Filaments Endpoint Tests
+ # =========================================================================
+
+ @pytest.mark.asyncio
+ @pytest.mark.integration
+ async def test_get_filaments_not_enabled(self, async_client: AsyncClient):
+ """Verify get filaments fails when not enabled."""
+ response = await async_client.get("/api/v1/spoolman/filaments")
+ assert response.status_code == 400
+ assert "not enabled" in response.json()["detail"].lower()
+
+ @pytest.mark.asyncio
+ @pytest.mark.integration
+ async def test_get_filaments_success(self, async_client: AsyncClient, spoolman_settings, mock_spoolman_client):
+ """Verify get filaments returns data in expected format."""
+ mock_filament = {
+ "id": 1,
+ "name": "PLA Basic",
+ "material": "PLA",
+ "color_hex": "FF0000",
+ "vendor_id": 1,
+ "weight": 1000,
+ }
+ mock_spoolman_client.get_filaments = AsyncMock(return_value=[mock_filament])
+
+ response = await async_client.get("/api/v1/spoolman/filaments")
+ assert response.status_code == 200
+ data = response.json()
+ assert "filaments" in data
+ assert isinstance(data["filaments"], list)
+ assert len(data["filaments"]) == 1
+ assert data["filaments"][0]["name"] == "PLA Basic"
diff --git a/frontend/src/__tests__/components/SpoolmanSettings.test.tsx b/frontend/src/__tests__/components/SpoolmanSettings.test.tsx
new file mode 100644
index 000000000..7df21116f
--- /dev/null
+++ b/frontend/src/__tests__/components/SpoolmanSettings.test.tsx
@@ -0,0 +1,314 @@
+/**
+ * Tests for the SpoolmanSettings component.
+ *
+ * Tests the Spoolman integration UI including:
+ * - Enable/disable toggle
+ * - URL configuration
+ * - Connection status
+ * - Sync functionality
+ */
+
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { screen, waitFor } from '@testing-library/react';
+import { render } from '../utils';
+import { SpoolmanSettings } from '../../components/SpoolmanSettings';
+
+// Mock the API client
+vi.mock('../../api/client', () => ({
+ api: {
+ getSettings: vi.fn().mockResolvedValue({}),
+ updateSettings: vi.fn().mockResolvedValue({}),
+ getSpoolmanStatus: vi.fn(),
+ connectSpoolman: vi.fn(),
+ disconnectSpoolman: vi.fn(),
+ syncAllPrintersAms: vi.fn(),
+ syncPrinterAms: vi.fn(),
+ getPrinters: vi.fn(),
+ },
+}));
+
+// Import mocked module
+import { api } from '../../api/client';
+
+// Mock fetch for Spoolman settings endpoints
+const mockFetchResponse = (data: object) => ({
+ ok: true,
+ json: () => Promise.resolve(data),
+});
+
+describe('SpoolmanSettings', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+
+ // Default API mocks
+ vi.mocked(api.getSpoolmanStatus).mockResolvedValue({
+ enabled: false,
+ connected: false,
+ url: null,
+ });
+ vi.mocked(api.getPrinters).mockResolvedValue([]);
+ vi.mocked(api.connectSpoolman).mockResolvedValue({ success: true, message: 'Connected' });
+ vi.mocked(api.disconnectSpoolman).mockResolvedValue({ success: true, message: 'Disconnected' });
+ vi.mocked(api.syncAllPrintersAms).mockResolvedValue({
+ success: true,
+ synced_count: 3,
+ skipped_count: 1,
+ skipped: [],
+ errors: [],
+ });
+
+ // Default fetch mock for settings - disabled state
+ global.fetch = vi.fn().mockImplementation((url: string) => {
+ if (url.includes('/api/v1/settings/spoolman')) {
+ return Promise.resolve(
+ mockFetchResponse({
+ spoolman_enabled: 'false',
+ spoolman_url: '',
+ spoolman_sync_mode: 'auto',
+ })
+ );
+ }
+ return Promise.reject(new Error('Unknown URL'));
+ }) as any;
+ });
+
+ describe('rendering', () => {
+ it('renders loading state initially', () => {
+ // Delay the fetch response to catch loading state
+ global.fetch = vi.fn().mockImplementation(() => new Promise(() => {})) as any;
+ render();
+
+ // Should show loading spinner
+ expect(document.querySelector('.animate-spin')).toBeInTheDocument();
+ });
+
+ it('renders component title', async () => {
+ render();
+
+ await waitFor(() => {
+ expect(screen.getByText('Spoolman Integration')).toBeInTheDocument();
+ });
+ });
+
+ it('renders enable toggle', async () => {
+ render();
+
+ await waitFor(() => {
+ expect(screen.getByText('Enable Spoolman')).toBeInTheDocument();
+ });
+ });
+
+ it('renders URL input', async () => {
+ render();
+
+ await waitFor(() => {
+ expect(screen.getByText('Spoolman URL')).toBeInTheDocument();
+ expect(screen.getByPlaceholderText('http://192.168.1.100:7912')).toBeInTheDocument();
+ });
+ });
+
+ it('renders sync mode selector', async () => {
+ render();
+
+ await waitFor(() => {
+ expect(screen.getByText('Sync Mode')).toBeInTheDocument();
+ });
+ });
+
+ it('renders info banner about sync', async () => {
+ render();
+
+ await waitFor(() => {
+ expect(screen.getByText('How Sync Works')).toBeInTheDocument();
+ expect(screen.getByText(/Only official Bambu Lab spools/)).toBeInTheDocument();
+ });
+ });
+ });
+
+ describe('disabled state', () => {
+ it('URL input is disabled when Spoolman is disabled', async () => {
+ render();
+
+ await waitFor(() => {
+ const urlInput = screen.getByPlaceholderText('http://192.168.1.100:7912');
+ expect(urlInput).toBeDisabled();
+ });
+ });
+
+ it('sync mode selector is disabled when Spoolman is disabled', async () => {
+ render();
+
+ await waitFor(() => {
+ // Find the select by its display value
+ const selectElement = screen.getByDisplayValue('Automatic');
+ expect(selectElement).toBeDisabled();
+ });
+ });
+
+ it('does not show connection status when disabled', async () => {
+ render();
+
+ await waitFor(() => {
+ expect(screen.getByText('Spoolman Integration')).toBeInTheDocument();
+ });
+
+ // Status section should not be visible when disabled
+ expect(screen.queryByText('Status:')).not.toBeInTheDocument();
+ });
+ });
+
+ describe('enabled state', () => {
+ beforeEach(() => {
+ global.fetch = vi.fn().mockImplementation((url: string) => {
+ if (url.includes('/api/v1/settings/spoolman')) {
+ if (url.includes('PUT') || (url as any).method === 'PUT') {
+ return Promise.resolve(
+ mockFetchResponse({
+ spoolman_enabled: 'true',
+ spoolman_url: 'http://localhost:7912',
+ spoolman_sync_mode: 'auto',
+ })
+ );
+ }
+ return Promise.resolve(
+ mockFetchResponse({
+ spoolman_enabled: 'true',
+ spoolman_url: 'http://localhost:7912',
+ spoolman_sync_mode: 'auto',
+ })
+ );
+ }
+ return Promise.reject(new Error('Unknown URL'));
+ }) as any;
+ });
+
+ it('URL input is enabled when Spoolman is enabled', async () => {
+ render();
+
+ await waitFor(() => {
+ const urlInput = screen.getByPlaceholderText('http://192.168.1.100:7912');
+ expect(urlInput).not.toBeDisabled();
+ });
+ });
+
+ it('shows connection status section when enabled', async () => {
+ render();
+
+ await waitFor(() => {
+ expect(screen.getByText('Status:')).toBeInTheDocument();
+ });
+ });
+
+ it('shows Disconnected when not connected', async () => {
+ vi.mocked(api.getSpoolmanStatus).mockResolvedValue({
+ enabled: true,
+ connected: false,
+ url: 'http://localhost:7912',
+ });
+
+ render();
+
+ await waitFor(() => {
+ expect(screen.getByText('Disconnected')).toBeInTheDocument();
+ });
+ });
+
+ it('shows Connect button when disconnected', async () => {
+ vi.mocked(api.getSpoolmanStatus).mockResolvedValue({
+ enabled: true,
+ connected: false,
+ url: 'http://localhost:7912',
+ });
+
+ render();
+
+ await waitFor(() => {
+ expect(screen.getByText('Connect')).toBeInTheDocument();
+ });
+ });
+
+ it('shows Connected and Disconnect button when connected', async () => {
+ vi.mocked(api.getSpoolmanStatus).mockResolvedValue({
+ enabled: true,
+ connected: true,
+ url: 'http://localhost:7912',
+ });
+
+ render();
+
+ await waitFor(() => {
+ expect(screen.getByText('Connected')).toBeInTheDocument();
+ expect(screen.getByText('Disconnect')).toBeInTheDocument();
+ });
+ });
+
+ it('shows sync section when connected', async () => {
+ vi.mocked(api.getSpoolmanStatus).mockResolvedValue({
+ enabled: true,
+ connected: true,
+ url: 'http://localhost:7912',
+ });
+
+ render();
+
+ await waitFor(() => {
+ expect(screen.getByText('Sync AMS Data')).toBeInTheDocument();
+ expect(screen.getByText('Sync')).toBeInTheDocument();
+ });
+ });
+
+ it('shows All Printers option in sync dropdown', async () => {
+ vi.mocked(api.getSpoolmanStatus).mockResolvedValue({
+ enabled: true,
+ connected: true,
+ url: 'http://localhost:7912',
+ });
+
+ render();
+
+ await waitFor(() => {
+ expect(screen.getByRole('option', { name: 'All Printers' })).toBeInTheDocument();
+ });
+ });
+ });
+
+ describe('sync mode options', () => {
+ it('shows Automatic option', async () => {
+ render();
+
+ await waitFor(() => {
+ expect(screen.getByRole('option', { name: 'Automatic' })).toBeInTheDocument();
+ });
+ });
+
+ it('shows Manual Only option', async () => {
+ render();
+
+ await waitFor(() => {
+ expect(screen.getByRole('option', { name: 'Manual Only' })).toBeInTheDocument();
+ });
+ });
+ });
+
+ describe('info text', () => {
+ it('shows URL help text', async () => {
+ render();
+
+ await waitFor(() => {
+ expect(
+ screen.getByText('URL of your Spoolman server (e.g., http://localhost:7912)')
+ ).toBeInTheDocument();
+ });
+ });
+
+ it('shows sync mode description for auto mode', async () => {
+ render();
+
+ await waitFor(() => {
+ expect(
+ screen.getByText('AMS data syncs automatically when changes are detected')
+ ).toBeInTheDocument();
+ });
+ });
+ });
+});