diff --git a/frontend/src/__tests__/components/SpoolFormModal.test.tsx b/frontend/src/__tests__/components/SpoolFormModal.test.tsx
index a13e49b64..f357fc121 100644
--- a/frontend/src/__tests__/components/SpoolFormModal.test.tsx
+++ b/frontend/src/__tests__/components/SpoolFormModal.test.tsx
@@ -62,6 +62,7 @@ const existingSpool: InventorySpool = {
rgba: 'FF0000FF',
label_weight: 1000,
core_weight: 250,
+ core_weight_catalog_id: null,
weight_used: 300,
slicer_filament: 'GFL99',
slicer_filament_name: 'Generic PLA',
@@ -183,4 +184,140 @@ describe('SpoolFormModal weightTouched', () => {
// weight_used MUST be included for new spools (default value 0)
expect(payload).toHaveProperty('weight_used', 0);
});
+
+ it('preserves core_weight_catalog_id when editing other fields', async () => {
+ const spoolWithCatalogId: InventorySpool = {
+ ...existingSpool,
+ core_weight_catalog_id: 5,
+ };
+
+ render(
+
+ );
+
+ await waitFor(() => {
+ expect(screen.getByText('Edit Spool')).toBeInTheDocument();
+ });
+
+ // Change the note field (unrelated to catalog ID)
+ const noteInputs = screen.getAllByPlaceholderText(/note/i);
+ expect(noteInputs.length).toBeGreaterThan(0);
+ fireEvent.change(noteInputs[0], { target: { value: 'Updated note' } });
+
+ // Click Save
+ const saveButton = screen.getByRole('button', { name: /save/i });
+ fireEvent.click(saveButton);
+
+ await waitFor(() => {
+ expect(api.updateSpool).toHaveBeenCalledTimes(1);
+ });
+
+ const [spoolId, payload] = vi.mocked(api.updateSpool).mock.calls[0];
+ expect(spoolId).toBe(1);
+ // core_weight_catalog_id MUST be preserved when editing other fields
+ expect(payload).toHaveProperty('core_weight_catalog_id', 5);
+ // Other changes should also be present
+ expect(payload).toHaveProperty('note', 'Updated note');
+ });
+
+ it('includes core_weight_catalog_id when selecting from catalog', async () => {
+ const mockCatalog = [
+ { id: 1, name: 'Generic 250g', weight: 250 },
+ { id: 2, name: 'Bambu Lab 250g', weight: 250 },
+ { id: 3, name: 'Standard 300g', weight: 300 },
+ ];
+
+ vi.mocked(api.getSpoolCatalog).mockResolvedValue(mockCatalog);
+
+ render(
+
+ );
+
+ await waitFor(() => {
+ expect(screen.getByRole('heading', { name: 'Add Spool' })).toBeInTheDocument();
+ });
+
+ // Wait for catalog to load
+ await waitFor(() => {
+ expect(api.getSpoolCatalog).toHaveBeenCalled();
+ });
+
+ // Click on the empty spool weight field to open dropdown
+ const weightInputs = screen.getAllByPlaceholderText(/search/i);
+ const weightPicker = weightInputs.find(input =>
+ input.getAttribute('placeholder')?.toLowerCase().includes('spool')
+ );
+ expect(weightPicker).toBeTruthy();
+ fireEvent.focus(weightPicker!);
+
+ // Click on "Bambu Lab 250g" option
+ await waitFor(() => {
+ const bambuOption = screen.queryByText('Bambu Lab 250g');
+ if (bambuOption) fireEvent.click(bambuOption);
+ });
+
+ // Click the add spool button
+ const addButtons = screen.getAllByRole('button', { name: /add spool/i });
+ const submitButton = addButtons.find(btn => btn.tagName === 'BUTTON' && btn.querySelector('svg.lucide-save'));
+ expect(submitButton).toBeTruthy();
+ fireEvent.click(submitButton!);
+
+ await waitFor(() => {
+ expect(api.createSpool).toHaveBeenCalledTimes(1);
+ });
+
+ const [payload] = vi.mocked(api.createSpool).mock.calls[0];
+ // Both weight AND catalog ID should be sent
+ expect(payload).toHaveProperty('core_weight', 250);
+ expect(payload).toHaveProperty('core_weight_catalog_id', 2); // ID of "Bambu Lab 250g"
+ });
+
+ it('displays correct catalog name when duplicates exist', async () => {
+ const spoolWithCatalogId: InventorySpool = {
+ ...existingSpool,
+ core_weight: 250,
+ core_weight_catalog_id: 2, // "Bambu Lab 250g", not the first match
+ };
+
+ const mockCatalog = [
+ { id: 1, name: 'Generic 250g', weight: 250 },
+ { id: 2, name: 'Bambu Lab 250g', weight: 250 },
+ { id: 3, name: 'Standard 300g', weight: 300 },
+ ];
+
+ vi.mocked(api.getSpoolCatalog).mockResolvedValue(mockCatalog);
+
+ render(
+
+ );
+
+ await waitFor(() => {
+ expect(screen.getByText('Edit Spool')).toBeInTheDocument();
+ });
+
+ // Wait for catalog to load
+ await waitFor(() => {
+ expect(api.getSpoolCatalog).toHaveBeenCalled();
+ });
+
+ // Should display "Bambu Lab 250g" (by ID), not "Generic 250g" (first match by weight)
+ await waitFor(() => {
+ const weightInputs = screen.getAllByDisplayValue(/250|Bambu/i);
+ const bambuFound = weightInputs.some(input =>
+ input.value === 'Bambu Lab 250g' || input.getAttribute('value') === 'Bambu Lab 250g'
+ );
+ expect(bambuFound || screen.queryByText('Bambu Lab 250g')).toBeTruthy();
+ });
+ });
});