| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312 |
- /**
- * Tests for bulk spool creation and quick-add mode.
- *
- * Verifies:
- * - Quick-add toggle appears only in create mode
- * - Quick-add mode shows brand and subtype as optional (no asterisk)
- * - Quick-add mode hides slicer preset field
- * - Quick-add mode hides PA Profile tab
- * - Quantity field is only rendered in quick-add mode
- * - Quantity field is hidden in edit mode
- * - Bulk create calls bulkCreateSpools when quantity > 1
- * - Single quantity calls createSpool as before
- * - validateForm with quickAdd=true only requires material
- */
- import React from 'react';
- import { describe, it, expect, vi, beforeEach } from 'vitest';
- import { screen, waitFor, fireEvent } from '@testing-library/react';
- import { render } from '../utils';
- import { SpoolFormModal } from '../../components/SpoolFormModal';
- import { validateForm, defaultFormData } from '../../components/spool-form/types';
- import type { InventorySpool } from '../../api/client';
- // Mock the API client
- vi.mock('../../api/client', () => ({
- api: {
- getSettings: vi.fn().mockResolvedValue({}),
- getAuthStatus: vi.fn().mockResolvedValue({ auth_enabled: false }),
- getCloudStatus: vi.fn().mockResolvedValue({ is_authenticated: false }),
- getFilamentPresets: vi.fn().mockResolvedValue([]),
- getSpoolCatalog: vi.fn().mockResolvedValue([]),
- getLocations: vi.fn().mockResolvedValue([]),
- getColorCatalog: vi.fn().mockResolvedValue([]),
- getLocalPresets: vi.fn().mockResolvedValue({ filament: [] }),
- getBuiltinFilaments: vi.fn().mockResolvedValue([]),
- getPrinters: vi.fn().mockResolvedValue([]),
- getSpoolUsageHistory: vi.fn().mockResolvedValue([]),
- createSpool: vi.fn().mockResolvedValue({ id: 99 }),
- bulkCreateSpools: vi.fn().mockResolvedValue([
- { id: 100, k_profiles: [] },
- { id: 101, k_profiles: [] },
- { id: 102, k_profiles: [] },
- ]),
- updateSpool: vi.fn().mockResolvedValue({ id: 1 }),
- saveSpoolKProfiles: vi.fn().mockResolvedValue([]),
- },
- }));
- // Mock the toast context
- const mockShowToast = vi.fn();
- vi.mock('../../contexts/ToastContext', async (importOriginal) => {
- const actual = await importOriginal<typeof import('../../contexts/ToastContext')>();
- return {
- ...actual,
- useToast: () => ({ showToast: mockShowToast }),
- };
- });
- const existingSpool: InventorySpool = {
- id: 1,
- material: 'PLA',
- subtype: 'Basic',
- brand: 'Polymaker',
- color_name: 'Red',
- rgba: 'FF0000FF',
- extra_colors: null,
- effect_type: null,
- label_weight: 1000,
- core_weight: 250,
- core_weight_catalog_id: null,
- weight_used: 300,
- slicer_filament: 'GFL99',
- slicer_filament_name: 'Generic PLA',
- nozzle_temp_min: null,
- nozzle_temp_max: null,
- note: null,
- added_full: null,
- last_used: null,
- encode_time: null,
- tag_uid: null,
- tray_uuid: null,
- data_origin: null,
- tag_type: null,
- archived_at: null,
- created_at: '2025-01-01T00:00:00Z',
- updated_at: '2025-01-01T00:00:00Z',
- k_profiles: [],
- cost_per_kg: null,
- };
- describe('validateForm with quickAdd', () => {
- it('requires only material in quick-add mode', () => {
- const result = validateForm({ ...defaultFormData, material: 'PLA' }, true);
- expect(result.isValid).toBe(true);
- expect(result.errors).toEqual({});
- });
- it('rejects empty material in quick-add mode', () => {
- const result = validateForm({ ...defaultFormData, material: '' }, true);
- expect(result.isValid).toBe(false);
- expect(result.errors.material).toBeDefined();
- });
- it('does not require slicer_filament in quick-add mode', () => {
- const result = validateForm(
- { ...defaultFormData, material: 'PETG', slicer_filament: '' },
- true,
- );
- expect(result.isValid).toBe(true);
- });
- it('does not require brand in quick-add mode', () => {
- const result = validateForm(
- { ...defaultFormData, material: 'ABS', brand: '' },
- true,
- );
- expect(result.isValid).toBe(true);
- });
- it('does not require subtype in quick-add mode', () => {
- const result = validateForm(
- { ...defaultFormData, material: 'TPU', subtype: '' },
- true,
- );
- expect(result.isValid).toBe(true);
- });
- it('requires all fields in full mode (quickAdd=false)', () => {
- const result = validateForm(defaultFormData, false);
- expect(result.isValid).toBe(false);
- expect(result.errors.material).toBeDefined();
- expect(result.errors.slicer_filament).toBeDefined();
- expect(result.errors.brand).toBeDefined();
- expect(result.errors.subtype).toBeDefined();
- });
- });
- describe('SpoolFormModal quick-add toggle', () => {
- beforeEach(() => {
- vi.clearAllMocks();
- });
- it('shows quick-add toggle in create mode', async () => {
- render(
- <SpoolFormModal
- isOpen={true}
- onClose={vi.fn()}
- mode="create"
- currencySymbol="$"
- />,
- );
- await waitFor(() => {
- expect(screen.getByRole('heading', { name: 'Add Spool' })).toBeInTheDocument();
- });
- expect(screen.getByText('Quick Add (Stock)')).toBeInTheDocument();
- });
- it('hides quick-add toggle in edit mode', async () => {
- render(
- <SpoolFormModal
- isOpen={true}
- onClose={vi.fn()}
- spool={existingSpool}
- mode="edit"
- currencySymbol="$"
- />,
- );
- await waitFor(() => {
- expect(screen.getByText('Edit Spool')).toBeInTheDocument();
- });
- expect(screen.queryByText('Quick Add (Stock)')).not.toBeInTheDocument();
- });
- it('hides PA Profile tab when quick-add is enabled', async () => {
- render(
- <SpoolFormModal
- isOpen={true}
- onClose={vi.fn()}
- mode="create"
- currencySymbol="$"
- />,
- );
- await waitFor(() => {
- expect(screen.getByRole('heading', { name: 'Add Spool' })).toBeInTheDocument();
- });
- // PA Profile tab should be visible initially
- expect(screen.getByText('PA Profile')).toBeInTheDocument();
- // Toggle quick-add on — the toggle is a button[role="switch"] sibling of the label
- const toggleButtons = screen.getAllByRole('button');
- const quickAddToggle = toggleButtons.find(btn =>
- btn.getAttribute('type') === 'button' &&
- btn.className.includes('rounded-full') &&
- btn.closest('div')?.textContent?.includes('Quick Add')
- );
- expect(quickAddToggle).toBeTruthy();
- fireEvent.click(quickAddToggle!);
- // PA Profile tab should be hidden
- await waitFor(() => {
- expect(screen.queryByText('PA Profile')).not.toBeInTheDocument();
- });
- });
- it('hides quantity field by default (non-quick-add)', async () => {
- render(
- <SpoolFormModal
- isOpen={true}
- onClose={vi.fn()}
- mode="create"
- currencySymbol="$"
- />,
- );
- await waitFor(() => {
- expect(screen.getByRole('heading', { name: 'Add Spool' })).toBeInTheDocument();
- });
- // Quantity field should NOT be visible in normal create mode
- expect(screen.queryByText('Quantity')).not.toBeInTheDocument();
- });
- it('shows quantity field only in quick-add mode', async () => {
- render(
- <SpoolFormModal
- isOpen={true}
- onClose={vi.fn()}
- mode="create"
- currencySymbol="$"
- />,
- );
- await waitFor(() => {
- expect(screen.getByRole('heading', { name: 'Add Spool' })).toBeInTheDocument();
- });
- // Toggle quick-add on
- const toggleButtons = screen.getAllByRole('button');
- const quickAddToggle = toggleButtons.find(btn =>
- btn.getAttribute('type') === 'button' &&
- btn.className.includes('rounded-full') &&
- btn.closest('div')?.textContent?.includes('Quick Add')
- );
- expect(quickAddToggle).toBeTruthy();
- fireEvent.click(quickAddToggle!);
- // Quantity field should now be visible
- await waitFor(() => {
- expect(screen.getByText('Quantity')).toBeInTheDocument();
- });
- });
- it('hides quantity field in edit mode', async () => {
- render(
- <SpoolFormModal
- isOpen={true}
- onClose={vi.fn()}
- spool={existingSpool}
- mode="edit"
- currencySymbol="$"
- />,
- );
- await waitFor(() => {
- expect(screen.getByText('Edit Spool')).toBeInTheDocument();
- });
- // Quantity field should NOT be visible in edit mode
- expect(screen.queryByText('Quantity')).not.toBeInTheDocument();
- });
- it('shows brand and subtype in quick-add mode without asterisk', async () => {
- render(
- <SpoolFormModal
- isOpen={true}
- onClose={vi.fn()}
- mode="create"
- currencySymbol="$"
- />,
- );
- await waitFor(() => {
- expect(screen.getByRole('heading', { name: 'Add Spool' })).toBeInTheDocument();
- });
- // Toggle quick-add on
- const toggleButtons = screen.getAllByRole('button');
- const quickAddToggle = toggleButtons.find(btn =>
- btn.getAttribute('type') === 'button' &&
- btn.className.includes('rounded-full') &&
- btn.closest('div')?.textContent?.includes('Quick Add')
- );
- fireEvent.click(quickAddToggle!);
- // Brand and Subtype should be visible (without asterisk = optional)
- await waitFor(() => {
- const brandLabel = screen.getByText('Brand');
- expect(brandLabel).toBeInTheDocument();
- expect(brandLabel.textContent).not.toContain('*');
- const subtypeLabel = screen.getByText('Subtype');
- expect(subtypeLabel).toBeInTheDocument();
- expect(subtypeLabel.textContent).not.toContain('*');
- });
- });
- });
|