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,452 @@
const PrintableLayoutService = require('../services/PrintableLayoutService');
// Mock the CodeGenerationService
jest.mock('../services/CodeGenerationService', () => {
return jest.fn().mockImplementation(() => ({
generateBarcode: jest.fn().mockResolvedValue({
success: true,
data: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
format: 'CODE128',
productCode: 'TEST123'
}),
generateQRCode: jest.fn().mockResolvedValue({
success: true,
data: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
productCode: 'TEST123',
embeddedData: { code: 'TEST123', desc: 'Test Product' }
})
}));
});
describe('PrintableLayoutService', () => {
let layoutService;
let sampleProducts;
beforeEach(() => {
layoutService = new PrintableLayoutService();
sampleProducts = [
{
product_code: 'PROD001',
description: 'Test Product 1',
category: 'Electronics',
unit_of_measure: 'pcs'
},
{
product_code: 'PROD002',
description: 'Test Product 2',
category: 'Hardware',
unit_of_measure: 'pcs'
},
{
product_code: 'PROD003',
description: 'Test Product 3 with a very long description that should be truncated',
category: 'Software',
unit_of_measure: 'licenses'
}
];
});
describe('Constructor and Configuration', () => {
test('should initialize with default options', () => {
expect(layoutService.labelSizes).toHaveProperty('avery-5160');
expect(layoutService.labelSizes).toHaveProperty('avery-5161');
expect(layoutService.labelSizes).toHaveProperty('custom');
expect(layoutService.defaultLayoutOptions.labelSize).toBe('avery-5160');
expect(layoutService.defaultLayoutOptions.includeBarcode).toBe(true);
expect(layoutService.defaultLayoutOptions.includeQRCode).toBe(false);
});
test('should return available label sizes', () => {
const sizes = layoutService.getAvailableLabelSizes();
expect(sizes).toHaveProperty('avery-5160');
expect(sizes).toHaveProperty('avery-5161');
expect(sizes).toHaveProperty('custom');
expect(sizes['avery-5160']).toHaveProperty('width');
expect(sizes['avery-5160']).toHaveProperty('height');
expect(sizes['avery-5160']).toHaveProperty('columns');
expect(sizes['avery-5160']).toHaveProperty('rows');
});
});
describe('Layout Preview Generation', () => {
test('should generate layout preview with default options', async () => {
const result = await layoutService.generateLayoutPreview(sampleProducts);
expect(result.success).toBe(true);
expect(result.preview).toHaveProperty('labelSize', 'avery-5160');
expect(result.preview).toHaveProperty('dimensions');
expect(result.preview).toHaveProperty('labelsPerPage');
expect(result.preview).toHaveProperty('totalPages');
expect(result.preview.totalPages).toBe(1); // 3 products, 30 labels per page
expect(result.preview.includeBarcode).toBe(true);
expect(result.preview.includeQRCode).toBe(false);
});
test('should generate layout preview with custom options', async () => {
const options = {
labelSize: 'avery-5161',
includeQRCode: true,
includeBarcode: false
};
const result = await layoutService.generateLayoutPreview(sampleProducts, options);
expect(result.success).toBe(true);
expect(result.preview.labelSize).toBe('avery-5161');
expect(result.preview.includeBarcode).toBe(false);
expect(result.preview.includeQRCode).toBe(true);
});
test('should calculate correct number of pages', async () => {
// Create more products to test pagination
const manyProducts = Array.from({ length: 35 }, (_, i) => ({
product_code: `PROD${String(i + 1).padStart(3, '0')}`,
description: `Product ${i + 1}`,
category: 'Test',
unit_of_measure: 'pcs'
}));
const result = await layoutService.generateLayoutPreview(manyProducts);
expect(result.success).toBe(true);
// avery-5160 has 30 labels per page (3 columns × 10 rows)
// 35 products should require 2 pages
expect(result.preview.totalPages).toBe(2);
});
test('should handle empty products array', async () => {
const result = await layoutService.generateLayoutPreview([]);
expect(result.success).toBe(true);
expect(result.preview.totalPages).toBe(0);
});
});
describe('Custom Template Generation', () => {
test('should generate custom template with valid dimensions', async () => {
const templateOptions = {
width: 40,
height: 20,
columns: 4,
rows: 10,
name: 'my-custom-template'
};
const result = await layoutService.generateCustomTemplate(templateOptions);
expect(result.success).toBe(true);
expect(result.template.name).toBe('my-custom-template');
expect(result.template.width).toBe(40);
expect(result.template.height).toBe(20);
expect(result.template.columns).toBe(4);
expect(result.template.rows).toBe(10);
expect(result.template.totalLabels).toBe(40);
expect(result.validation.fitsOnPage).toBe(true);
});
test('should reject template that exceeds page width', async () => {
const templateOptions = {
width: 100,
height: 20,
columns: 5,
rows: 5
};
const result = await layoutService.generateCustomTemplate(templateOptions);
expect(result.success).toBe(false);
expect(result.error).toContain('Template width exceeds page width');
});
test('should reject template that exceeds page height', async () => {
const templateOptions = {
width: 30,
height: 50,
columns: 2,
rows: 10
};
const result = await layoutService.generateCustomTemplate(templateOptions);
expect(result.success).toBe(false);
expect(result.error).toContain('Template height exceeds page height');
});
test('should use default values for missing template options', async () => {
const result = await layoutService.generateCustomTemplate({});
expect(result.success).toBe(true);
expect(result.template.width).toBe(50);
expect(result.template.height).toBe(25);
expect(result.template.columns).toBe(4);
expect(result.template.rows).toBe(8);
expect(result.template.name).toBe('custom-template');
});
});
describe('PDF Layout Generation', () => {
test('should generate PDF with barcode layout', async () => {
const options = {
labelSize: 'avery-5160',
includeBarcode: true,
includeQRCode: false,
includeProductCode: true,
includeDescription: true
};
const result = await layoutService.generatePrintableLayout(sampleProducts, options);
expect(result.success).toBe(true);
expect(result.data).toBeInstanceOf(Buffer);
expect(result.metadata.totalProducts).toBe(3);
expect(result.metadata.totalPages).toBe(1);
expect(result.metadata.labelSize).toBe('avery-5160');
expect(result.metadata.format).toBe('PDF');
});
test('should generate PDF with QR code layout', async () => {
const options = {
labelSize: 'avery-5161',
includeBarcode: false,
includeQRCode: true,
includeProductCode: true,
includeDescription: false
};
const result = await layoutService.generatePrintableLayout(sampleProducts, options);
expect(result.success).toBe(true);
expect(result.data).toBeInstanceOf(Buffer);
expect(result.metadata.labelSize).toBe('avery-5161');
});
test('should generate PDF with both barcode and QR code', async () => {
const options = {
includeBarcode: true,
includeQRCode: true,
includeProductCode: true,
includeDescription: true
};
const result = await layoutService.generatePrintableLayout(sampleProducts, options);
expect(result.success).toBe(true);
expect(result.data).toBeInstanceOf(Buffer);
});
test('should handle empty products array', async () => {
const result = await layoutService.generatePrintableLayout([]);
expect(result.success).toBe(false);
expect(result.error).toContain('Products array is required and cannot be empty');
});
test('should handle null products parameter', async () => {
const result = await layoutService.generatePrintableLayout(null);
expect(result.success).toBe(false);
expect(result.error).toContain('Products array is required and cannot be empty');
});
test('should generate multiple pages for many products', async () => {
// Create enough products to span multiple pages
const manyProducts = Array.from({ length: 35 }, (_, i) => ({
product_code: `PROD${String(i + 1).padStart(3, '0')}`,
description: `Product ${i + 1}`,
category: 'Test',
unit_of_measure: 'pcs'
}));
const result = await layoutService.generatePrintableLayout(manyProducts);
expect(result.success).toBe(true);
expect(result.metadata.totalProducts).toBe(35);
expect(result.metadata.totalPages).toBe(2);
});
test('should use custom label size', async () => {
const options = {
labelSize: 'custom',
includeBarcode: true
};
const result = await layoutService.generatePrintableLayout(sampleProducts, options);
expect(result.success).toBe(true);
expect(result.metadata.labelSize).toBe('custom');
});
test('should handle code generation failures gracefully', async () => {
// Mock code generation service to return failure
layoutService.codeGenService.generateBarcode.mockResolvedValueOnce({
success: false,
error: 'Barcode generation failed'
});
const result = await layoutService.generatePrintableLayout(sampleProducts);
// Should still succeed but without the failed barcode
expect(result.success).toBe(true);
});
});
describe('Configuration Export/Import', () => {
test('should export layout configuration', () => {
const layoutConfig = {
labelSize: 'avery-5160',
includeBarcode: true,
includeQRCode: false,
fontSize: 10
};
const exported = layoutService.exportLayoutConfiguration(layoutConfig);
expect(exported).toHaveProperty('version', '1.0');
expect(exported).toHaveProperty('timestamp');
expect(exported.configuration).toEqual({
...layoutConfig,
availableSizes: Object.keys(layoutService.labelSizes)
});
});
test('should import valid layout configuration', () => {
const configData = {
version: '1.0',
timestamp: '2023-01-01T00:00:00.000Z',
configuration: {
labelSize: 'avery-5161',
includeBarcode: true,
includeQRCode: true,
fontSize: 12
}
};
const result = layoutService.importLayoutConfiguration(configData);
expect(result.success).toBe(true);
expect(result.configuration.labelSize).toBe('avery-5161');
expect(result.message).toBe('Configuration imported successfully');
});
test('should reject invalid configuration format', () => {
const invalidConfig = {
version: '1.0',
// missing configuration property
};
const result = layoutService.importLayoutConfiguration(invalidConfig);
expect(result.success).toBe(false);
expect(result.error).toBe('Invalid configuration format');
});
test('should reject configuration with missing required fields', () => {
const configData = {
configuration: {
includeBarcode: true
// missing labelSize
}
};
const result = layoutService.importLayoutConfiguration(configData);
expect(result.success).toBe(false);
expect(result.error).toContain('Missing required field: labelSize');
});
test('should reject configuration with unsupported label size', () => {
const configData = {
configuration: {
labelSize: 'unsupported-size',
includeBarcode: true
}
};
const result = layoutService.importLayoutConfiguration(configData);
expect(result.success).toBe(false);
expect(result.error).toContain('Unsupported label size: unsupported-size');
});
});
describe('Text Truncation', () => {
test('should truncate long text to fit width', () => {
// Create a mock PDF object for text measurement
const mockPdf = {
getTextWidth: jest.fn((text) => text.length * 2) // Simple mock: 2mm per character
};
const longText = 'This is a very long product description that should be truncated';
const maxWidth = 50; // 25 characters max at 2mm per character
const truncated = layoutService._truncateText(longText, maxWidth, mockPdf);
expect(truncated).toContain('...');
expect(mockPdf.getTextWidth(truncated)).toBeLessThanOrEqual(maxWidth);
});
test('should return original text if it fits', () => {
const mockPdf = {
getTextWidth: jest.fn((text) => text.length * 2)
};
const shortText = 'Short text';
const maxWidth = 50;
const result = layoutService._truncateText(shortText, maxWidth, mockPdf);
expect(result).toBe(shortText);
});
test('should handle empty text', () => {
const mockPdf = {
getTextWidth: jest.fn(() => 0)
};
const result = layoutService._truncateText('', 50, mockPdf);
expect(result).toBe('');
});
test('should handle null text', () => {
const mockPdf = {
getTextWidth: jest.fn(() => 0)
};
const result = layoutService._truncateText(null, 50, mockPdf);
expect(result).toBe('');
});
});
describe('Private Methods', () => {
test('should generate codes for products', async () => {
const options = {
includeBarcode: true,
includeQRCode: true,
barcodeFormat: 'CODE128'
};
const codes = await layoutService._generateCodesForProducts(sampleProducts, options);
expect(codes).toHaveLength(3);
expect(codes[0]).toHaveProperty('product');
expect(codes[0]).toHaveProperty('barcode');
expect(codes[0]).toHaveProperty('qrCode');
expect(codes[0].barcode.success).toBe(true);
expect(codes[0].qrCode.success).toBe(true);
});
test('should generate codes with selective options', async () => {
const options = {
includeBarcode: true,
includeQRCode: false
};
const codes = await layoutService._generateCodesForProducts(sampleProducts, options);
expect(codes[0].barcode).toBeTruthy();
expect(codes[0].qrCode).toBeNull();
});
});
});