Initial commit: Inventory Barcode System

This commit is contained in:
2025-07-22 20:24:51 -04:00
commit 511b01748d
63 changed files with 26932 additions and 0 deletions

View File

@ -0,0 +1,613 @@
/**
* 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 = `
<div class="status-indicator"></div>
<span class="status-text">Ready</span>
`;
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();
});
});
});