Files
inventory-barcode-system/__tests__/frontend.import.test.js

416 lines
15 KiB
JavaScript

/**
* 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');
});
});
});