/** * Frontend Scanning Interface Tests * Tests the scanning UI functionality including camera access, product lookup, and inventory updates */ // Mock DOM environment for testing const { JSDOM } = require('jsdom'); const fs = require('fs'); const path = require('path'); describe('Scanning Interface', () => { let dom; let document; let window; let InventoryApp; beforeEach(() => { // Load the HTML file const htmlPath = path.join(__dirname, '../public/index.html'); const htmlContent = fs.readFileSync(htmlPath, 'utf8'); // Create JSDOM instance dom = new JSDOM(htmlContent, { url: 'http://localhost:3000', pretendToBeVisual: true, resources: 'usable' }); document = dom.window.document; window = dom.window; // Mock global objects global.document = document; global.window = window; global.FormData = window.FormData; global.fetch = jest.fn(); global.navigator = { mediaDevices: { getUserMedia: jest.fn() } }; // Mock file API global.File = class File { constructor(parts, filename, options = {}) { this.name = filename; this.size = parts.reduce((size, part) => size + part.length, 0); this.type = options.type || ''; } }; // Load the JavaScript application const jsPath = path.join(__dirname, '../public/js/app.js'); let jsContent = fs.readFileSync(jsPath, 'utf8'); // Modify the JS content to expose InventoryApp class jsContent = jsContent.replace( 'class InventoryApp {', 'window.InventoryApp = class InventoryApp {' ); // Execute the JavaScript in the JSDOM context const script = new window.Function(jsContent); script.call(window); // Get the InventoryApp class from the window context InventoryApp = window.InventoryApp; }); afterEach(() => { jest.clearAllMocks(); dom.window.close(); }); describe('Initialization', () => { test('should initialize scanning state correctly', () => { const app = new InventoryApp(); expect(app.cameraStream).toBeNull(); expect(app.isScanning).toBe(false); expect(app.currentProduct).toBeNull(); expect(app.recentUpdates).toEqual([]); expect(app.scanInterval).toBeNull(); }); test('should set up scanning event listeners', () => { const app = new InventoryApp(); // Check that key elements exist expect(document.getElementById('start-camera')).toBeTruthy(); expect(document.getElementById('stop-camera')).toBeTruthy(); expect(document.getElementById('manual-code-input')).toBeTruthy(); expect(document.getElementById('lookup-code')).toBeTruthy(); }); test('should initialize with stop camera button disabled', () => { new InventoryApp(); const stopButton = document.getElementById('stop-camera'); expect(stopButton.disabled).toBe(true); }); }); describe('Camera Controls', () => { test('should start camera successfully', async () => { const app = new InventoryApp(); // Mock successful camera access const mockStream = { getTracks: () => [{ stop: jest.fn() }] }; global.navigator.mediaDevices.getUserMedia.mockResolvedValueOnce(mockStream); await app.startCamera(); expect(app.cameraStream).toBe(mockStream); expect(app.isScanning).toBe(true); expect(document.getElementById('camera-container').style.display).toBe('block'); }); test('should handle camera access failure', async () => { const app = new InventoryApp(); app.showError = jest.fn(); global.navigator.mediaDevices.getUserMedia.mockRejectedValueOnce(new Error('Camera not available')); await app.startCamera(); expect(app.showError).toHaveBeenCalledWith('Failed to access camera: Camera not available'); expect(app.cameraStream).toBeNull(); expect(app.isScanning).toBe(false); }); test('should stop camera correctly', () => { const app = new InventoryApp(); // Mock active camera stream const mockTrack = { stop: jest.fn() }; app.cameraStream = { getTracks: () => [mockTrack] }; app.isScanning = true; app.scanInterval = setInterval(() => {}, 1000); app.stopCamera(); expect(mockTrack.stop).toHaveBeenCalled(); expect(app.cameraStream).toBeNull(); expect(app.isScanning).toBe(false); expect(app.scanInterval).toBeNull(); expect(document.getElementById('camera-container').style.display).toBe('none'); }); test('should handle camera not supported', async () => { const app = new InventoryApp(); app.showError = jest.fn(); // Mock no camera support global.navigator.mediaDevices = undefined; await app.startCamera(); expect(app.showError).toHaveBeenCalledWith('Failed to access camera: Camera not supported in this browser'); }); }); describe('Scan Status Updates', () => { test('should update scan status correctly', () => { const app = new InventoryApp(); // Add status elements to DOM const statusContainer = document.getElementById('scan-status'); statusContainer.innerHTML = `
Ready `; statusContainer.style.display = 'flex'; app.updateScanStatus('Scanning...', 'scanning'); const statusText = document.querySelector('.status-text'); const statusIndicator = document.querySelector('.status-indicator'); expect(statusText.textContent).toBe('Scanning...'); expect(statusIndicator.classList.contains('scanning')).toBe(true); }); test('should handle scanned code', () => { const app = new InventoryApp(); app.updateScanStatus = jest.fn(); app.lookupProduct = jest.fn(); app.handleScannedCode('ABC123'); expect(app.updateScanStatus).toHaveBeenCalledWith('Code detected!', 'ready'); expect(document.getElementById('manual-code-input').value).toBe('ABC123'); expect(app.lookupProduct).toHaveBeenCalledWith('ABC123'); }); }); describe('Product Lookup', () => { test('should lookup product successfully from server', async () => { const app = new InventoryApp(); app.displayProductInfo = jest.fn(); const mockProduct = { id: 1, product_code: 'ABC123', description: 'Widget A', quantity: 50 }; global.fetch.mockResolvedValueOnce({ ok: true, json: async () => mockProduct }); await app.lookupProduct('ABC123'); expect(app.displayProductInfo).toHaveBeenCalledWith(mockProduct); }); test('should handle product not found', async () => { const app = new InventoryApp(); app.showError = jest.fn(); global.fetch.mockResolvedValueOnce({ ok: false, status: 404 }); await app.lookupProduct('INVALID'); expect(app.showError).toHaveBeenCalledWith('Product with code "INVALID" not found'); }); test('should use mock data when server unavailable', async () => { const app = new InventoryApp(); app.mockProductLookup = jest.fn(); global.fetch.mockRejectedValueOnce(new Error('Network error')); await app.lookupProduct('ABC123'); expect(app.mockProductLookup).toHaveBeenCalledWith('ABC123'); }); test('should find mock product correctly', () => { const app = new InventoryApp(); app.displayProductInfo = jest.fn(); app.mockProductLookup('ABC123'); expect(app.displayProductInfo).toHaveBeenCalledWith( expect.objectContaining({ product_code: 'ABC123', description: 'Widget A' }) ); }); test('should handle mock product not found', () => { const app = new InventoryApp(); app.showError = jest.fn(); app.mockProductLookup('INVALID'); expect(app.showError).toHaveBeenCalledWith('Product with code "INVALID" not found'); }); test('should handle manual code entry with Enter key', () => { const app = new InventoryApp(); app.lookupProduct = jest.fn(); const input = document.getElementById('manual-code-input'); input.value = 'ABC123'; const event = new window.KeyboardEvent('keypress', { key: 'Enter' }); input.dispatchEvent(event); expect(app.lookupProduct).toHaveBeenCalledWith('ABC123'); }); }); describe('Product Information Display', () => { test('should display product information correctly', () => { const app = new InventoryApp(); app.setupInventoryUpdate = jest.fn(); const mockProduct = { id: 1, product_code: 'ABC123', description: 'Widget A', category: 'Widgets', quantity: 50, unit_of_measure: 'pcs' }; app.displayProductInfo(mockProduct); expect(app.currentProduct).toBe(mockProduct); expect(document.getElementById('product-info-section').style.display).toBe('block'); expect(app.setupInventoryUpdate).toHaveBeenCalledWith(mockProduct); const productInfoCard = document.getElementById('product-info-card'); expect(productInfoCard.innerHTML).toContain('ABC123'); expect(productInfoCard.innerHTML).toContain('Widget A'); expect(productInfoCard.innerHTML).toContain('50 pcs'); }); }); describe('Inventory Update', () => { test('should setup inventory update correctly', () => { const app = new InventoryApp(); app.updateNewLevelPreview = jest.fn(); const mockProduct = { id: 1, product_code: 'ABC123', quantity: 50 }; app.setupInventoryUpdate(mockProduct); expect(document.getElementById('current-level').textContent).toBe('50'); expect(document.getElementById('inventory-update-section').style.display).toBe('block'); expect(document.getElementById('confirm-update').disabled).toBe(false); expect(app.updateNewLevelPreview).toHaveBeenCalled(); }); test('should update new level preview correctly for set operation', () => { const app = new InventoryApp(); app.currentProduct = { quantity: 50 }; // Set form values document.querySelector('input[name="update-type"][value="set"]').checked = true; document.getElementById('quantity-input').value = '75'; app.updateNewLevelPreview(); expect(document.getElementById('new-level-preview').textContent).toBe('75'); }); test('should update new level preview correctly for add operation', () => { const app = new InventoryApp(); app.currentProduct = { quantity: 50 }; // Set form values document.querySelector('input[name="update-type"][value="add"]').checked = true; document.getElementById('quantity-input').value = '25'; app.updateNewLevelPreview(); expect(document.getElementById('new-level-preview').textContent).toBe('75'); }); test('should update new level preview correctly for subtract operation', () => { const app = new InventoryApp(); app.currentProduct = { quantity: 50 }; // Set form values document.querySelector('input[name="update-type"][value="subtract"]').checked = true; document.getElementById('quantity-input').value = '20'; app.updateNewLevelPreview(); expect(document.getElementById('new-level-preview').textContent).toBe('30'); }); test('should prevent negative inventory levels', () => { const app = new InventoryApp(); app.currentProduct = { quantity: 10 }; // Set form values document.querySelector('input[name="update-type"][value="subtract"]').checked = true; document.getElementById('quantity-input').value = '20'; app.updateNewLevelPreview(); expect(document.getElementById('new-level-preview').textContent).toBe('0'); }); test('should show custom reason input when other is selected', () => { const app = new InventoryApp(); const reasonSelect = document.getElementById('update-reason'); const customReasonInput = document.getElementById('custom-reason'); reasonSelect.value = 'other'; reasonSelect.dispatchEvent(new window.Event('change')); expect(customReasonInput.style.display).toBe('block'); expect(customReasonInput.required).toBe(true); }); test('should hide custom reason input when other option is not selected', () => { const app = new InventoryApp(); const reasonSelect = document.getElementById('update-reason'); const customReasonInput = document.getElementById('custom-reason'); reasonSelect.value = 'stock_count'; reasonSelect.dispatchEvent(new window.Event('change')); expect(customReasonInput.style.display).toBe('none'); expect(customReasonInput.required).toBe(false); }); }); describe('Inventory Update Confirmation', () => { test('should confirm inventory update successfully', async () => { const app = new InventoryApp(); app.showSuccess = jest.fn(); app.addRecentUpdate = jest.fn(); app.cancelInventoryUpdate = jest.fn(); app.currentProduct = { id: 1, product_code: 'ABC123', description: 'Widget A', quantity: 50 }; // Set form values document.querySelector('input[name="update-type"][value="set"]').checked = true; document.getElementById('quantity-input').value = '75'; document.getElementById('update-reason').value = 'stock_count'; global.fetch.mockResolvedValueOnce({ ok: true, json: async () => ({ success: true }) }); await app.confirmInventoryUpdate(); expect(app.showSuccess).toHaveBeenCalledWith('Inventory updated successfully! New level: 75'); expect(app.currentProduct.quantity).toBe(75); expect(app.addRecentUpdate).toHaveBeenCalled(); expect(app.cancelInventoryUpdate).toHaveBeenCalled(); }); test('should handle inventory update failure gracefully', async () => { const app = new InventoryApp(); app.showSuccess = jest.fn(); app.addRecentUpdate = jest.fn(); app.cancelInventoryUpdate = jest.fn(); app.currentProduct = { id: 1, product_code: 'ABC123', description: 'Widget A', quantity: 50 }; // Set form values document.querySelector('input[name="update-type"][value="set"]').checked = true; document.getElementById('quantity-input').value = '75'; global.fetch.mockRejectedValueOnce(new Error('Network error')); await app.confirmInventoryUpdate(); expect(app.showSuccess).toHaveBeenCalledWith('Inventory updated successfully! New level: 75'); expect(app.addRecentUpdate).toHaveBeenCalled(); expect(app.cancelInventoryUpdate).toHaveBeenCalled(); }); test('should cancel inventory update correctly', () => { const app = new InventoryApp(); app.currentProduct = { id: 1 }; document.getElementById('manual-code-input').value = 'ABC123'; app.cancelInventoryUpdate(); expect(app.currentProduct).toBeNull(); expect(document.getElementById('product-info-section').style.display).toBe('none'); expect(document.getElementById('inventory-update-section').style.display).toBe('none'); expect(document.getElementById('manual-code-input').value).toBe(''); }); }); describe('Recent Updates', () => { test('should add recent update correctly', () => { const app = new InventoryApp(); app.displayRecentUpdates = jest.fn(); const update = { product_code: 'ABC123', description: 'Widget A', old_level: 50, new_level: 75, change: 25, reason: 'stock_count', timestamp: new Date() }; app.addRecentUpdate(update); expect(app.recentUpdates).toContain(update); expect(app.displayRecentUpdates).toHaveBeenCalled(); }); test('should limit recent updates to 10 items', () => { const app = new InventoryApp(); app.displayRecentUpdates = jest.fn(); // Add 12 updates for (let i = 0; i < 12; i++) { app.addRecentUpdate({ product_code: `CODE${i}`, description: `Product ${i}`, old_level: i, new_level: i + 1, change: 1, timestamp: new Date() }); } expect(app.recentUpdates.length).toBe(10); expect(app.recentUpdates[0].product_code).toBe('CODE11'); // Most recent first }); test('should display recent updates correctly', () => { const app = new InventoryApp(); const update = { product_code: 'ABC123', description: 'Widget A', old_level: 50, new_level: 75, change: 25, reason: 'stock_count', timestamp: new Date() }; app.recentUpdates = [update]; app.displayRecentUpdates(); const recentUpdatesContainer = document.getElementById('recent-updates'); expect(recentUpdatesContainer.innerHTML).toContain('ABC123'); expect(recentUpdatesContainer.innerHTML).toContain('Widget A'); expect(recentUpdatesContainer.innerHTML).toContain('+25'); expect(recentUpdatesContainer.innerHTML).toContain('50 → 75'); }); test('should show no updates message when list is empty', () => { const app = new InventoryApp(); app.recentUpdates = []; app.displayRecentUpdates(); const recentUpdatesContainer = document.getElementById('recent-updates'); expect(recentUpdatesContainer.innerHTML).toContain('No recent updates'); }); test('should format time correctly', () => { const app = new InventoryApp(); const testDate = new Date('2023-01-01T14:30:00'); const formatted = app.formatTime(testDate); expect(formatted).toMatch(/\d{1,2}:\d{2}/); // Should match time format }); }); describe('UI Interactions', () => { test('should switch to scan tab correctly', () => { const app = new InventoryApp(); app.switchTab('scan'); expect(app.currentTab).toBe('scan'); const activeTab = document.querySelector('.nav-tab.active'); expect(activeTab.dataset.tab).toBe('scan'); const activeContent = document.querySelector('.tab-content.active'); expect(activeContent.id).toBe('scan-tab'); }); test('should handle scan tab click', () => { const app = new InventoryApp(); const scanTab = document.querySelector('[data-tab="scan"]'); scanTab.click(); expect(app.currentTab).toBe('scan'); }); test('should update new level preview when quantity input changes', () => { const app = new InventoryApp(); app.updateNewLevelPreview = jest.fn(); const quantityInput = document.getElementById('quantity-input'); quantityInput.value = '100'; quantityInput.dispatchEvent(new window.Event('input')); expect(app.updateNewLevelPreview).toHaveBeenCalled(); }); test('should update new level preview when update type changes', () => { const app = new InventoryApp(); app.updateNewLevelPreview = jest.fn(); const addRadio = document.querySelector('input[name="update-type"][value="add"]'); addRadio.checked = true; addRadio.dispatchEvent(new window.Event('change')); expect(app.updateNewLevelPreview).toHaveBeenCalled(); }); }); });