Initial commit: Inventory Barcode System
This commit is contained in:
613
__tests__/frontend.scanning.test.js
Normal file
613
__tests__/frontend.scanning.test.js
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user