mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-17 19:36:28 +02:00
Add SpoolBuddy backend + frontend test coverage
21 backend integration tests for all 12 SpoolBuddy API endpoints (device register/re-register/list, heartbeat status/commands/404/ offline broadcast, NFC tag match/unmatch/removal, scale reading/ weight calculation/missing spool, calibration tare/set-tare/ set-factor/zero-delta/get) and 20 frontend component tests for WeightDisplay, SpoolInfoCard, UnknownTagCard, and TagDetectedModal.
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { screen, fireEvent, waitFor } from '@testing-library/react';
|
||||
import { render } from '../utils';
|
||||
import { SpoolInfoCard, UnknownTagCard } from '../../components/spoolbuddy/SpoolInfoCard';
|
||||
import type { MatchedSpool } from '../../hooks/useSpoolBuddyState';
|
||||
|
||||
const mockUpdateSpoolWeight = vi.fn();
|
||||
|
||||
vi.mock('../../api/client', () => ({
|
||||
api: {
|
||||
getSettings: vi.fn().mockResolvedValue({}),
|
||||
getAuthStatus: vi.fn().mockResolvedValue({ auth_enabled: false }),
|
||||
},
|
||||
spoolbuddyApi: {
|
||||
updateSpoolWeight: (...args: unknown[]) => mockUpdateSpoolWeight(...args),
|
||||
},
|
||||
}));
|
||||
|
||||
const mockSpool: MatchedSpool = {
|
||||
id: 42,
|
||||
tag_uid: 'AABBCCDD11223344',
|
||||
material: 'PLA',
|
||||
subtype: 'Matte',
|
||||
color_name: 'Jade White',
|
||||
rgba: 'E8F5E9FF',
|
||||
brand: 'Bambu',
|
||||
label_weight: 1000,
|
||||
core_weight: 250,
|
||||
weight_used: 200,
|
||||
};
|
||||
|
||||
describe('SpoolInfoCard', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockUpdateSpoolWeight.mockResolvedValue({ status: 'ok', weight_used: 300 });
|
||||
});
|
||||
|
||||
it('renders spool material, brand, color name', () => {
|
||||
render(<SpoolInfoCard spool={mockSpool} scaleWeight={null} />);
|
||||
|
||||
expect(screen.getByText('Jade White')).toBeInTheDocument();
|
||||
expect(screen.getByText(/Bambu/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/PLA/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows spool color circle with correct hex color', () => {
|
||||
const { container } = render(<SpoolInfoCard spool={mockSpool} scaleWeight={null} />);
|
||||
|
||||
// SpoolIcon renders an SVG circle with fill=colorHex
|
||||
const circle = container.querySelector('circle[fill="#E8F5E9"]');
|
||||
expect(circle).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows remaining weight and fill percentage', () => {
|
||||
// scaleWeight=900g, core=250g → remaining = 900-250 = 650g
|
||||
// fillPercent = round(650/1000 * 100) = 65%
|
||||
render(<SpoolInfoCard spool={mockSpool} scaleWeight={900} />);
|
||||
|
||||
expect(screen.getByText('650g')).toBeInTheDocument();
|
||||
expect(screen.getByText('65%')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls onAssignToAms when "Assign to AMS" button clicked', () => {
|
||||
const onAssign = vi.fn();
|
||||
render(
|
||||
<SpoolInfoCard spool={mockSpool} scaleWeight={800} onAssignToAms={onAssign} />
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByText('Assign to AMS'));
|
||||
expect(onAssign).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('calls onSyncWeight when sync button clicked', async () => {
|
||||
const onSync = vi.fn();
|
||||
render(
|
||||
<SpoolInfoCard spool={mockSpool} scaleWeight={800} onSyncWeight={onSync} />
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByText('Sync Weight'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUpdateSpoolWeight).toHaveBeenCalledWith(42, 800);
|
||||
});
|
||||
});
|
||||
|
||||
it('calls onClose when close button clicked', () => {
|
||||
const onClose = vi.fn();
|
||||
render(
|
||||
<SpoolInfoCard spool={mockSpool} scaleWeight={null} onClose={onClose} />
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByText('Close'));
|
||||
expect(onClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('UnknownTagCard', () => {
|
||||
it('renders tag UID', () => {
|
||||
render(<UnknownTagCard tagUid="DEADBEEF12345678" scaleWeight={null} />);
|
||||
|
||||
expect(screen.getByText('DEADBEEF12345678')).toBeInTheDocument();
|
||||
expect(screen.getByText('New Tag Detected')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows "Add to Inventory" button', () => {
|
||||
const onAdd = vi.fn();
|
||||
render(
|
||||
<UnknownTagCard tagUid="DEADBEEF" scaleWeight={null} onAddToInventory={onAdd} />
|
||||
);
|
||||
|
||||
const btn = screen.getByText('Add to Inventory');
|
||||
expect(btn).toBeInTheDocument();
|
||||
fireEvent.click(btn);
|
||||
expect(onAdd).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,110 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { screen, fireEvent } from '@testing-library/react';
|
||||
import { render } from '../utils';
|
||||
import { TagDetectedModal } from '../../components/spoolbuddy/TagDetectedModal';
|
||||
import type { MatchedSpool } from '../../hooks/useSpoolBuddyState';
|
||||
|
||||
const mockUpdateSpoolWeight = vi.fn();
|
||||
|
||||
vi.mock('../../api/client', () => ({
|
||||
api: {
|
||||
getSettings: vi.fn().mockResolvedValue({}),
|
||||
getAuthStatus: vi.fn().mockResolvedValue({ auth_enabled: false }),
|
||||
},
|
||||
spoolbuddyApi: {
|
||||
updateSpoolWeight: (...args: unknown[]) => mockUpdateSpoolWeight(...args),
|
||||
},
|
||||
}));
|
||||
|
||||
const mockSpool: MatchedSpool = {
|
||||
id: 7,
|
||||
tag_uid: 'AA11BB22CC33DD44',
|
||||
material: 'PETG',
|
||||
subtype: 'HF',
|
||||
color_name: 'Orange',
|
||||
rgba: 'FF6600FF',
|
||||
brand: 'Overture',
|
||||
label_weight: 1000,
|
||||
core_weight: 250,
|
||||
weight_used: 100,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
isOpen: true,
|
||||
onClose: vi.fn(),
|
||||
spool: mockSpool,
|
||||
tagUid: 'AA11BB22CC33DD44',
|
||||
scaleWeight: 950.0,
|
||||
weightStable: true,
|
||||
onSyncWeight: vi.fn(),
|
||||
onAssignToAms: vi.fn(),
|
||||
onLinkSpool: vi.fn(),
|
||||
onAddToInventory: vi.fn(),
|
||||
};
|
||||
|
||||
describe('TagDetectedModal', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockUpdateSpoolWeight.mockResolvedValue({ status: 'ok', weight_used: 300 });
|
||||
});
|
||||
|
||||
it('does not render when isOpen=false', () => {
|
||||
render(<TagDetectedModal {...defaultProps} isOpen={false} />);
|
||||
expect(screen.queryByText('Spool Detected')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders known spool view when spool provided', () => {
|
||||
render(<TagDetectedModal {...defaultProps} />);
|
||||
|
||||
expect(screen.getByText('Spool Detected')).toBeInTheDocument();
|
||||
expect(screen.getByText('Orange')).toBeInTheDocument();
|
||||
expect(screen.getByText(/Overture/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/PETG/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders unknown tag view when spool is null', () => {
|
||||
render(
|
||||
<TagDetectedModal
|
||||
{...defaultProps}
|
||||
spool={null}
|
||||
tagUid="DEADBEEF11223344"
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText('New Tag Detected')).toBeInTheDocument();
|
||||
expect(screen.getByText('DEADBEEF11223344')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('closes on Escape key', () => {
|
||||
const onClose = vi.fn();
|
||||
render(<TagDetectedModal {...defaultProps} onClose={onClose} />);
|
||||
|
||||
fireEvent.keyDown(document, { key: 'Escape' });
|
||||
expect(onClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('shows weight from scale', () => {
|
||||
// scaleWeight=950g, core=250g → remaining = 950-250 = 700g
|
||||
render(<TagDetectedModal {...defaultProps} scaleWeight={950} />);
|
||||
|
||||
expect(screen.getByText('700g')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows action buttons (Assign to AMS, Sync Weight)', () => {
|
||||
const onAssign = vi.fn();
|
||||
const onSync = vi.fn();
|
||||
render(
|
||||
<TagDetectedModal
|
||||
{...defaultProps}
|
||||
onAssignToAms={onAssign}
|
||||
onSyncWeight={onSync}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText('Assign to AMS')).toBeInTheDocument();
|
||||
expect(screen.getByText('Sync Weight')).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByText('Assign to AMS'));
|
||||
expect(onAssign).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,80 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { screen, fireEvent, waitFor } from '@testing-library/react';
|
||||
import { render } from '../utils';
|
||||
import { WeightDisplay } from '../../components/spoolbuddy/WeightDisplay';
|
||||
|
||||
const mockTare = vi.fn();
|
||||
|
||||
vi.mock('../../api/client', () => ({
|
||||
api: {
|
||||
getSettings: vi.fn().mockResolvedValue({}),
|
||||
getAuthStatus: vi.fn().mockResolvedValue({ auth_enabled: false }),
|
||||
},
|
||||
spoolbuddyApi: {
|
||||
tare: (...args: unknown[]) => mockTare(...args),
|
||||
},
|
||||
}));
|
||||
|
||||
const defaultProps = {
|
||||
weight: 823.4,
|
||||
weightStable: true,
|
||||
deviceOnline: true,
|
||||
deviceId: 'sb-0001',
|
||||
};
|
||||
|
||||
describe('WeightDisplay', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockTare.mockResolvedValue({ status: 'ok' });
|
||||
});
|
||||
|
||||
it('renders weight value with 1 decimal place', () => {
|
||||
render(<WeightDisplay {...defaultProps} weight={823.456} />);
|
||||
expect(screen.getByText('823.5')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows green dot when stable and online', () => {
|
||||
const { container } = render(
|
||||
<WeightDisplay {...defaultProps} weightStable={true} deviceOnline={true} />
|
||||
);
|
||||
const dot = container.querySelector('.bg-green-500');
|
||||
expect(dot).toBeInTheDocument();
|
||||
expect(screen.getByText('Stable')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows amber dot when unstable', () => {
|
||||
const { container } = render(
|
||||
<WeightDisplay {...defaultProps} weightStable={false} deviceOnline={true} />
|
||||
);
|
||||
const dot = container.querySelector('.bg-amber-500');
|
||||
expect(dot).toBeInTheDocument();
|
||||
expect(screen.getByText('Measuring...')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows gray dot when offline', () => {
|
||||
const { container } = render(
|
||||
<WeightDisplay {...defaultProps} deviceOnline={false} />
|
||||
);
|
||||
const dot = container.querySelector('.bg-zinc-600');
|
||||
expect(dot).toBeInTheDocument();
|
||||
expect(screen.getByText('No reading')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('tare button calls spoolbuddyApi.tare(deviceId)', async () => {
|
||||
render(<WeightDisplay {...defaultProps} />);
|
||||
|
||||
const tareButton = screen.getByText('Tare');
|
||||
fireEvent.click(tareButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockTare).toHaveBeenCalledWith('sb-0001');
|
||||
});
|
||||
});
|
||||
|
||||
it('tare button is disabled when no deviceId', () => {
|
||||
render(<WeightDisplay {...defaultProps} deviceId={null} />);
|
||||
|
||||
const tareButton = screen.getByText('Tare');
|
||||
expect(tareButton).toBeDisabled();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user