/** * Frontend Excel Import Interface Tests * Tests the Excel import UI functionality including drag-and-drop, validation, and progress indicators */ // Mock DOM environment for testing const { JSDOM } = require('jsdom'); const fs = require('fs'); const path = require('path'); describe('Excel Import 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(); // 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 with correct default state', () => { const app = new InventoryApp(); expect(app.currentTab).toBe('import'); expect(app.uploadedFile).toBeNull(); expect(app.previewData).toBeNull(); expect(app.validationErrors).toEqual([]); }); test('should set up event listeners correctly', () => { const app = new InventoryApp(); // Check that upload area exists and has click handler const uploadArea = document.getElementById('upload-area'); expect(uploadArea).toBeTruthy(); // Check that file input exists const fileInput = document.getElementById('file-input'); expect(fileInput).toBeTruthy(); expect(fileInput.accept).toBe('.xlsx,.xls'); }); test('should initialize with import tab active', () => { new InventoryApp(); const activeTab = document.querySelector('.nav-tab.active'); expect(activeTab.dataset.tab).toBe('import'); const activeContent = document.querySelector('.tab-content.active'); expect(activeContent.id).toBe('import-tab'); }); }); describe('Tab Navigation', () => { test('should switch tabs correctly', () => { const app = new InventoryApp(); app.switchTab('generate'); expect(app.currentTab).toBe('generate'); const activeTab = document.querySelector('.nav-tab.active'); expect(activeTab.dataset.tab).toBe('generate'); const activeContent = document.querySelector('.tab-content.active'); expect(activeContent.id).toBe('generate-tab'); }); test('should handle tab clicks', () => { const app = new InventoryApp(); const generateTab = document.querySelector('[data-tab="generate"]'); generateTab.click(); expect(app.currentTab).toBe('generate'); }); }); describe('File Upload Validation', () => { test('should accept valid Excel files', async () => { const app = new InventoryApp(); app.showError = jest.fn(); app.processFile = jest.fn(); const validFile = new File(['test content'], 'test.xlsx', { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }); await app.handleFileUpload(validFile); expect(app.showError).not.toHaveBeenCalled(); expect(app.processFile).toHaveBeenCalledWith(validFile); expect(app.uploadedFile).toBe(validFile); }); test('should reject invalid file types', async () => { const app = new InventoryApp(); app.showError = jest.fn(); app.processFile = jest.fn(); const invalidFile = new File(['test content'], 'test.txt', { type: 'text/plain' }); await app.handleFileUpload(invalidFile); expect(app.showError).toHaveBeenCalledWith('Please select a valid Excel file (.xlsx or .xls)'); expect(app.processFile).not.toHaveBeenCalled(); }); test('should reject files that are too large', async () => { const app = new InventoryApp(); app.showError = jest.fn(); app.processFile = jest.fn(); // Create a mock file that's too large (>10MB) const largeFile = new File(['x'.repeat(11 * 1024 * 1024)], 'large.xlsx', { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }); await app.handleFileUpload(largeFile); expect(app.showError).toHaveBeenCalledWith('File size must be less than 10MB'); expect(app.processFile).not.toHaveBeenCalled(); }); }); describe('File Processing', () => { test('should show progress during file processing', async () => { const app = new InventoryApp(); app.updateProgress = jest.fn(); // Mock successful API response global.fetch.mockResolvedValueOnce({ ok: true, json: async () => ({ success: true, data: [], errors: [], stats: { total: 0, valid: 0, invalid: 0 } }) }); const file = new File(['test'], 'test.xlsx', { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }); await app.processFile(file); expect(app.updateProgress).toHaveBeenCalledWith(10, 'Reading file...'); expect(app.updateProgress).toHaveBeenCalledWith(30, 'Uploading file...'); expect(app.updateProgress).toHaveBeenCalledWith(60, 'Parsing data...'); expect(app.updateProgress).toHaveBeenCalledWith(80, 'Validating data...'); expect(app.updateProgress).toHaveBeenCalledWith(100, 'Complete!'); }); test('should handle server errors gracefully', async () => { const app = new InventoryApp(); app.showMockPreview = jest.fn(); // Mock fetch to simulate network error global.fetch.mockRejectedValueOnce(new TypeError('Failed to fetch')); const file = new File(['test'], 'test.xlsx', { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }); await app.processFile(file); expect(app.showMockPreview).toHaveBeenCalled(); }); }); describe('Data Preview', () => { test('should display preview data correctly', () => { const app = new InventoryApp(); const mockResult = { success: true, data: [ { row: 1, product_code: 'ABC123', description: 'Widget A', quantity: 50, valid: true }, { row: 2, product_code: '', description: 'Widget B', quantity: 25, valid: false, error: 'Missing product code' } ], errors: ['Row 2: Missing product code'], stats: { total: 2, valid: 1, invalid: 1 } }; app.displayPreview(mockResult); // Check stats display expect(document.getElementById('success-count').textContent).toBe('1 valid rows'); expect(document.getElementById('error-count').textContent).toBe('1 errors'); expect(document.getElementById('total-count').textContent).toBe('2 total rows'); // Check error list const errorList = document.getElementById('error-list'); expect(errorList.classList.contains('hidden')).toBe(false); // Check preview table const tbody = document.getElementById('preview-tbody'); expect(tbody.children.length).toBe(2); // Check import button state const confirmButton = document.getElementById('confirm-import'); expect(confirmButton.disabled).toBe(false); }); test('should disable import button when no valid rows', () => { const app = new InventoryApp(); const mockResult = { success: true, data: [ { row: 1, product_code: '', description: 'Widget A', quantity: 50, valid: false, error: 'Missing product code' } ], errors: ['Row 1: Missing product code'], stats: { total: 1, valid: 0, invalid: 1 } }; app.displayPreview(mockResult); const confirmButton = document.getElementById('confirm-import'); expect(confirmButton.disabled).toBe(true); }); test('should hide error list when no errors', () => { const app = new InventoryApp(); const mockResult = { success: true, data: [ { row: 1, product_code: 'ABC123', description: 'Widget A', quantity: 50, valid: true } ], errors: [], stats: { total: 1, valid: 1, invalid: 0 } }; app.displayPreview(mockResult); const errorList = document.getElementById('error-list'); expect(errorList.classList.contains('hidden')).toBe(true); }); }); describe('Import Confirmation', () => { test('should handle successful import', async () => { const app = new InventoryApp(); app.showSuccess = jest.fn(); app.resetImportState = jest.fn(); app.previewData = { data: [ { row: 1, product_code: 'ABC123', description: 'Widget A', quantity: 50, valid: true } ], stats: { valid: 1 } }; app.uploadedFile = new File(['test'], 'test.xlsx'); global.fetch.mockResolvedValueOnce({ ok: true, json: async () => ({ imported: 1 }) }); await app.confirmImport(); expect(app.showSuccess).toHaveBeenCalledWith('Successfully imported 1 products!'); expect(app.resetImportState).toHaveBeenCalled(); }); test('should handle import failure gracefully', async () => { const app = new InventoryApp(); app.showSuccess = jest.fn(); app.resetImportState = jest.fn(); app.previewData = { data: [ { row: 1, product_code: 'ABC123', description: 'Widget A', quantity: 50, valid: true } ], stats: { valid: 1 } }; app.uploadedFile = new File(['test'], 'test.xlsx'); global.fetch.mockRejectedValueOnce(new Error('Network error')); await app.confirmImport(); // Should still show success for demo purposes expect(app.showSuccess).toHaveBeenCalledWith('Successfully imported 1 products!'); expect(app.resetImportState).toHaveBeenCalled(); }); }); describe('State Management', () => { test('should reset import state correctly', () => { const app = new InventoryApp(); // Set some state app.uploadedFile = new File(['test'], 'test.xlsx'); app.previewData = { data: [] }; app.validationErrors = ['error']; // Show preview container document.getElementById('preview-container').style.display = 'block'; app.resetImportState(); expect(app.uploadedFile).toBeNull(); expect(app.previewData).toBeNull(); expect(app.validationErrors).toEqual([]); expect(document.getElementById('preview-container').style.display).toBe('none'); }); }); describe('Progress Indicators', () => { test('should show and update progress correctly', () => { const app = new InventoryApp(); app.showProgress(); const progressContainer = document.getElementById('progress-container'); expect(progressContainer.style.display).toBe('block'); app.updateProgress(50, 'Processing...'); const progressFill = document.getElementById('progress-fill'); const progressText = document.getElementById('progress-text'); expect(progressFill.style.width).toBe('50%'); expect(progressText.textContent).toBe('Processing...'); }); test('should hide progress when complete', () => { const app = new InventoryApp(); app.showProgress(); app.hideProgress(); const progressContainer = document.getElementById('progress-container'); expect(progressContainer.style.display).toBe('none'); }); }); describe('Notifications', () => { test('should show error notifications', () => { const app = new InventoryApp(); app.showError('Test error message'); const errorNotification = document.querySelector('.error-notification'); expect(errorNotification).toBeTruthy(); expect(errorNotification.textContent).toBe('Test error message'); }); test('should show success notifications', () => { const app = new InventoryApp(); app.showSuccess('Test success message'); const successNotification = document.querySelector('.success-notification'); expect(successNotification).toBeTruthy(); expect(successNotification.textContent).toBe('Test success message'); }); }); });