mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-22 03:49:18 +02:00
Auto-detect subnet for printer discovery
The Add Printer dialog previously required users to manually enter their network subnet for scanning (defaulting to 192.168.1.0/24). Now the backend detects available network interfaces and returns their subnets via the /discovery/info endpoint. The frontend auto-selects the first detected subnet and shows a dropdown when multiple subnets are available, falling back to a text input if none are detected.
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
/**
|
||||
* Tests for AddPrinterModal discovery subnet auto-detection.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { render } from '../utils';
|
||||
import { PrintersPage } from '../../pages/PrintersPage';
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import { server } from '../mocks/server';
|
||||
|
||||
const mockPrinters = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'X1 Carbon',
|
||||
ip_address: '192.168.1.100',
|
||||
serial_number: '00M09A350100001',
|
||||
access_code: '12345678',
|
||||
model: 'X1C',
|
||||
enabled: true,
|
||||
nozzle_diameter: 0.4,
|
||||
nozzle_type: 'hardened_steel',
|
||||
location: null,
|
||||
auto_archive: true,
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
updated_at: '2024-01-01T00:00:00Z',
|
||||
},
|
||||
];
|
||||
|
||||
const mockPrinterStatus = {
|
||||
connected: true,
|
||||
state: 'IDLE',
|
||||
progress: 0,
|
||||
layer_num: 0,
|
||||
total_layers: 0,
|
||||
temperatures: { nozzle: 25, bed: 25, chamber: 25 },
|
||||
remaining_time: 0,
|
||||
filename: null,
|
||||
wifi_signal: -50,
|
||||
};
|
||||
|
||||
describe('AddPrinterModal Discovery', () => {
|
||||
beforeEach(() => {
|
||||
server.use(
|
||||
http.get('/api/v1/printers/', () => {
|
||||
return HttpResponse.json(mockPrinters);
|
||||
}),
|
||||
http.get('/api/v1/printers/:id/status', () => {
|
||||
return HttpResponse.json(mockPrinterStatus);
|
||||
}),
|
||||
http.get('/api/v1/queue/', () => {
|
||||
return HttpResponse.json([]);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('auto-populates subnet from discovery info in Docker mode', async () => {
|
||||
server.use(
|
||||
http.get('/api/v1/discovery/info', () => {
|
||||
return HttpResponse.json({
|
||||
is_docker: true,
|
||||
ssdp_running: false,
|
||||
scan_running: false,
|
||||
subnets: ['10.0.0.0/24'],
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
render(<PrintersPage />);
|
||||
|
||||
// Wait for printer page to load
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('X1 Carbon')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Click the Add Printer button
|
||||
const addButton = screen.getByText(/add printer/i);
|
||||
await userEvent.click(addButton);
|
||||
|
||||
// Wait for the modal and discovery info to load
|
||||
await waitFor(() => {
|
||||
// Should show subnet dropdown with detected subnet
|
||||
const subnetSelect = screen.getByDisplayValue('10.0.0.0/24');
|
||||
expect(subnetSelect).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('shows dropdown when multiple subnets detected in Docker mode', async () => {
|
||||
server.use(
|
||||
http.get('/api/v1/discovery/info', () => {
|
||||
return HttpResponse.json({
|
||||
is_docker: true,
|
||||
ssdp_running: false,
|
||||
scan_running: false,
|
||||
subnets: ['192.168.1.0/24', '10.0.0.0/24'],
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
render(<PrintersPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('X1 Carbon')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const addButton = screen.getByText(/add printer/i);
|
||||
await userEvent.click(addButton);
|
||||
|
||||
await waitFor(() => {
|
||||
// Should show a select element (dropdown) with both subnets
|
||||
const selectElement = screen.getByDisplayValue('192.168.1.0/24');
|
||||
expect(selectElement.tagName).toBe('SELECT');
|
||||
|
||||
// Both options should be available
|
||||
const options = selectElement.querySelectorAll('option');
|
||||
expect(options).toHaveLength(2);
|
||||
expect(options[0].textContent).toBe('192.168.1.0/24');
|
||||
expect(options[1].textContent).toBe('10.0.0.0/24');
|
||||
});
|
||||
});
|
||||
|
||||
it('shows text input when no subnets detected in Docker mode', async () => {
|
||||
server.use(
|
||||
http.get('/api/v1/discovery/info', () => {
|
||||
return HttpResponse.json({
|
||||
is_docker: true,
|
||||
ssdp_running: false,
|
||||
scan_running: false,
|
||||
subnets: [],
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
render(<PrintersPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('X1 Carbon')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const addButton = screen.getByText(/add printer/i);
|
||||
await userEvent.click(addButton);
|
||||
|
||||
await waitFor(() => {
|
||||
// Should show a text input with placeholder
|
||||
const textInput = screen.getByPlaceholderText('192.168.1.0/24');
|
||||
expect(textInput).toBeInTheDocument();
|
||||
expect(textInput.tagName).toBe('INPUT');
|
||||
});
|
||||
});
|
||||
|
||||
it('does not show subnet field in non-Docker mode', async () => {
|
||||
server.use(
|
||||
http.get('/api/v1/discovery/info', () => {
|
||||
return HttpResponse.json({
|
||||
is_docker: false,
|
||||
ssdp_running: false,
|
||||
scan_running: false,
|
||||
subnets: ['192.168.1.0/24'],
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
render(<PrintersPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('X1 Carbon')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const addButton = screen.getByText(/add printer/i);
|
||||
await userEvent.click(addButton);
|
||||
|
||||
await waitFor(() => {
|
||||
// Should show the discover button but NOT the subnet field
|
||||
expect(screen.getByText(/discover printers/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Subnet field should not exist
|
||||
expect(screen.queryByPlaceholderText('192.168.1.0/24')).not.toBeInTheDocument();
|
||||
expect(screen.queryByDisplayValue('192.168.1.0/24')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -364,6 +364,19 @@ export const handlers = [
|
||||
return new HttpResponse(null, { status: 204 });
|
||||
}),
|
||||
|
||||
// ========================================================================
|
||||
// Discovery
|
||||
// ========================================================================
|
||||
|
||||
http.get('/api/v1/discovery/info', () => {
|
||||
return HttpResponse.json({
|
||||
is_docker: false,
|
||||
ssdp_running: false,
|
||||
scan_running: false,
|
||||
subnets: ['192.168.1.0/24'],
|
||||
});
|
||||
}),
|
||||
|
||||
// ========================================================================
|
||||
// Version / Health
|
||||
// ========================================================================
|
||||
|
||||
@@ -3934,6 +3934,7 @@ export interface DiscoveryInfo {
|
||||
is_docker: boolean;
|
||||
ssdp_running: boolean;
|
||||
scan_running: boolean;
|
||||
subnets: string[];
|
||||
}
|
||||
|
||||
export interface SubnetScanStatus {
|
||||
|
||||
@@ -3385,13 +3385,18 @@ function AddPrinterModal({
|
||||
const [discoveryError, setDiscoveryError] = useState('');
|
||||
const [hasScanned, setHasScanned] = useState(false);
|
||||
const [isDocker, setIsDocker] = useState(false);
|
||||
const [subnet, setSubnet] = useState('192.168.1.0/24');
|
||||
const [detectedSubnets, setDetectedSubnets] = useState<string[]>([]);
|
||||
const [subnet, setSubnet] = useState('');
|
||||
const [scanProgress, setScanProgress] = useState({ scanned: 0, total: 0 });
|
||||
|
||||
// Fetch discovery info on mount
|
||||
useEffect(() => {
|
||||
discoveryApi.getInfo().then(info => {
|
||||
setIsDocker(info.is_docker);
|
||||
if (info.subnets.length > 0) {
|
||||
setDetectedSubnets(info.subnets);
|
||||
setSubnet(info.subnets[0]);
|
||||
}
|
||||
}).catch(() => {
|
||||
// Ignore errors, assume not Docker
|
||||
});
|
||||
@@ -3556,14 +3561,27 @@ function AddPrinterModal({
|
||||
<label className="block text-sm text-bambu-gray mb-1">
|
||||
{t('printers.discovery.subnetToScan')}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none text-sm"
|
||||
value={subnet}
|
||||
onChange={(e) => setSubnet(e.target.value)}
|
||||
placeholder="192.168.1.0/24"
|
||||
disabled={discovering}
|
||||
/>
|
||||
{detectedSubnets.length > 0 ? (
|
||||
<select
|
||||
className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none text-sm"
|
||||
value={subnet}
|
||||
onChange={(e) => setSubnet(e.target.value)}
|
||||
disabled={discovering}
|
||||
>
|
||||
{detectedSubnets.map(s => (
|
||||
<option key={s} value={s}>{s}</option>
|
||||
))}
|
||||
</select>
|
||||
) : (
|
||||
<input
|
||||
type="text"
|
||||
className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none text-sm"
|
||||
value={subnet}
|
||||
onChange={(e) => setSubnet(e.target.value)}
|
||||
placeholder="192.168.1.0/24"
|
||||
disabled={discovering}
|
||||
/>
|
||||
)}
|
||||
<p className="mt-1 text-xs text-bambu-gray">
|
||||
{t('printers.discovery.dockerNote')}
|
||||
</p>
|
||||
|
||||
Reference in New Issue
Block a user