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